diff --git a/pocs/private-payment/shielded-pool-extension/.env.example b/pocs/private-payment/shielded-pool-extension/.env.example new file mode 100644 index 0000000..5dfed15 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/.env.example @@ -0,0 +1,9 @@ +PRIVATE_KEY= +SEPOLIA_RPC_URL= +ATTESTATION_REGISTRY_ADDRESS=0x0000000000000000000000000000000000000000 +DEPOSIT_VERIFIER_ADDRESS=0x0000000000000000000000000000000000000000 +WITHDRAW_VERIFIER_ADDRESS=0x0000000000000000000000000000000000000000 +TRANSFER_VERIFIER_ADDRESS=0x0000000000000000000000000000000000000000 +ETHERSCAN_API_KEY= +USE_MOCK_VERIFIER=true +PIR_SERVER_URL=http://localhost:7777 diff --git a/pocs/private-payment/shielded-pool-extension/.gitignore b/pocs/private-payment/shielded-pool-extension/.gitignore new file mode 100644 index 0000000..5e0f44f --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/.gitignore @@ -0,0 +1,9 @@ +.env +dependencies/ +target/ +**/Prover.toml +contracts/out/ +contracts/cache/ +contracts/broadcast/**/31337/** +deployments.toml +**/.DS_Store diff --git a/pocs/private-payment/shielded-pool-extension/.rustfmt.toml b/pocs/private-payment/shielded-pool-extension/.rustfmt.toml new file mode 100644 index 0000000..be31798 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/.rustfmt.toml @@ -0,0 +1,8 @@ +max_width = 90 # changed +normalize_comments = true # changed +imports_layout = "Vertical" # changed +imports_granularity = "Crate" # changed +trailing_semicolon = false # changed +edition = "2024" # changed +use_try_shorthand = true # changed +use_field_init_shorthand = true # changed diff --git a/pocs/private-payment/shielded-pool-extension/Cargo.lock b/pocs/private-payment/shielded-pool-extension/Cargo.lock new file mode 100644 index 0000000..3efac31 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/Cargo.lock @@ -0,0 +1,4809 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alloy" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07dc44b606f29348ce7c127e7f872a6d2df3cfeff85b7d6bba62faca75112fdd" +dependencies = [ + "alloy-consensus", + "alloy-contract", + "alloy-core", + "alloy-eips", + "alloy-genesis", + "alloy-network", + "alloy-node-bindings", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types", + "alloy-serde", + "alloy-signer", + "alloy-signer-local", + "alloy-transport", + "alloy-transport-http", + "alloy-trie", +] + +[[package]] +name = "alloy-chains" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e0378e959aa6a885897522080a990e80eb317f1e9a222a604492ea50e13096" +dependencies = [ + "alloy-primitives", + "num_enum", + "strum", +] + +[[package]] +name = "alloy-consensus" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4ff99651d46cef43767b5e8262ea228cd05287409ccb0c947cc25e70a952f9" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-trie", + "alloy-tx-macros", + "auto_impl", + "borsh", + "c-kzg", + "derive_more", + "either", + "k256", + "once_cell", + "rand 0.8.6", + "secp256k1 0.30.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-consensus-any" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0701b0eda8051a2398591113e7862f807ccdd3315d0b441f06c2a0865a379b" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-contract" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c83c7a3c4e1151e8cac383d0a67ddf358f37e5ea51c95a1283d897c9de0a5a" +dependencies = [ + "alloy-consensus", + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-network", + "alloy-network-primitives", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-sol-types", + "alloy-transport", + "futures", + "futures-util", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-core" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ddde5968de6044d67af107ad835bc0069a7ca245870b94c5958a7d8712b184" +dependencies = [ + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-primitives", + "alloy-rlp", + "alloy-sol-types", +] + +[[package]] +name = "alloy-dyn-abi" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a475bb02d9cef2dbb99065c1664ab3fe1f9352e21d6d5ed3f02cdbfc06ed1abc" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-type-parser", + "alloy-sol-types", + "itoa", + "serde", + "serde_json", + "winnow 1.0.3", +] + +[[package]] +name = "alloy-eip2124" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-eip2930" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-eip7928" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "407510740da514b694fecb44d8b3cebdc60d448f70cc5d24743e8ba273448a6e" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "once_cell", + "serde", +] + +[[package]] +name = "alloy-eips" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def1626eea28d48c6cc0a6f16f34d4af0001906e4f889df6c660b39c86fd044d" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-eip7928", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "auto_impl", + "borsh", + "c-kzg", + "derive_more", + "either", + "serde", + "serde_with", + "sha2", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-genesis" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55d9d1aba3f914f0e8db9e4616ae37f3d811426d95bdccf44e47d0605ab202f6" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "alloy-trie", + "borsh", + "serde", + "serde_with", +] + +[[package]] +name = "alloy-hardforks" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165210652f71dfc094b051602bafd691f506c54050a174b1cba18fb5ef706a3" +dependencies = [ + "alloy-chains", + "alloy-eip2124", + "alloy-primitives", + "auto_impl", + "dyn-clone", +] + +[[package]] +name = "alloy-json-abi" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c36c9d7f9021601b04bfef14a4b64849f6d73116a4e91e071d7fbfe10247901" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-json-rpc" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57586581f2008933241d16c3e3f633168b3a5d2738c5c42ea5246ec5e0ef17a" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "http", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "alloy-network" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b36c2a0ed74e48851f78415ca5b465211bd678891ba11e88fee09eac534bab1" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-json-rpc", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-types-any", + "alloy-rpc-types-eth", + "alloy-serde", + "alloy-signer", + "alloy-sol-types", + "async-trait", + "auto_impl", + "derive_more", + "futures-utils-wasm", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-network-primitives" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "636c8051da58802e757b76c3b65af610b95799f72423dc955737dec73de234fd" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-node-bindings" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ce6930ce52e43b0768dc99ceeff5cb9e673e8c9f87d926914cd028b2e3f7233" +dependencies = [ + "alloy-genesis", + "alloy-hardforks", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "alloy-signer-local", + "k256", + "libc", + "rand 0.8.6", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "tracing", + "url", +] + +[[package]] +name = "alloy-primitives" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4885c1409b6936c4898e646ef58baf6ec54edaf6d8179f79df805a7b85b7cf3e" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.4", + "rapidhash", + "ruint", + "rustc-hash", + "secp256k1 0.31.1", + "serde", + "sha3", +] + +[[package]] +name = "alloy-provider" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3dd56e2eafe8b1803e325867ac2c8a4c73c9fb5f341ffd8347f9344458c5922" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-json-rpc", + "alloy-network", + "alloy-network-primitives", + "alloy-node-bindings", + "alloy-primitives", + "alloy-rpc-client", + "alloy-rpc-types-anvil", + "alloy-rpc-types-eth", + "alloy-signer", + "alloy-sol-types", + "alloy-transport", + "alloy-transport-http", + "async-stream", + "async-trait", + "auto_impl", + "dashmap", + "either", + "futures", + "futures-utils-wasm", + "lru", + "parking_lot", + "pin-project", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc90b1e703d3c03f4ff7f48e82dd0bc1c8211ab7d079cd836a06fcfeb06651cb" +dependencies = [ + "alloy-rlp-derive", + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36834a5c0a2fa56e171bf256c34d70fca07d0c0031583edea1c4946b7889c9e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "alloy-rpc-client" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91577235d341a1bdbee30a463655d08504408a4d51e9f72edbfc5a622829f402" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-transport", + "alloy-transport-http", + "futures", + "pin-project", + "reqwest", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-rpc-types" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cff039bf01a17d76c0aace3a3a773d5f895eb4c68baaae729ec9da9e86c99c" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-rpc-types-anvil" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22250cf438b6a3926de67683c08163bfa1fd1efa47ee9512cbcd631b6b0243c" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-rpc-types-any" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73234a141ecce14e2989748c04fcac23deee67a445e2c4c167cfb42d4dacd1b6" +dependencies = [ + "alloy-consensus-any", + "alloy-rpc-types-eth", + "alloy-serde", +] + +[[package]] +name = "alloy-rpc-types-eth" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010e101dbebe0c678248907a2545b574a87d078d82c2f6f5d0e8e7c9a6149a10" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-sol-types", + "itertools 0.14.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-serde" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e6d631f8b975229361d8af7b2c749af31c73b3cf1352f90e144ddb06227105e" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-signer" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97f40010b5e8f79b70bf163b38cd15f529b18ca88c4427c0e43441ee54e4ed82" +dependencies = [ + "alloy-primitives", + "async-trait", + "auto_impl", + "either", + "elliptic-curve", + "k256", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-signer-local" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c4ec1cc27473819399a3f0da83bc1cef0ceaac8c1c93997696e46dc74377a58" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "k256", + "rand 0.8.6", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840128ed2b2971d6d4668a553fe403a82683d3acc646c73e75887e7157408033" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63ec265e5d65d725175f6ca7711c970824c90ef9c0d1f1973711d4150ee612dd" +dependencies = [ + "alloy-json-abi", + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap 2.14.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "sha3", + "syn 2.0.117", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bf01077f18650876cfa682eb1f949967b5cde03f1a51c955c469d2c9b4aa67" +dependencies = [ + "alloy-json-abi", + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.117", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "857b470ecdd2ed38beaf82ad1a38c516a8ff75266750f38b9eeed001d575241b" +dependencies = [ + "serde", + "winnow 1.0.3", +] + +[[package]] +name = "alloy-sol-types" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384cf252de0db2dec52821eac037a7f57e2aa33fe5b900ce6fe39973402341f1" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "serde", +] + +[[package]] +name = "alloy-transport" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a03bb3f02b9a7ab23dacd1822fa7f69aa5c8eefcdcf57fad085e0b8d76fb4334" +dependencies = [ + "alloy-json-rpc", + "auto_impl", + "base64", + "derive_more", + "futures", + "futures-utils-wasm", + "parking_lot", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tower", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-transport-http" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce599598ef8ebe067f3627509358d9faaa1ef94f77f834a7783cd44209ef55c" +dependencies = [ + "alloy-json-rpc", + "alloy-transport", + "itertools 0.14.0", + "reqwest", + "serde_json", + "tower", + "tracing", + "url", +] + +[[package]] +name = "alloy-trie" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "derive_more", + "nybbles", + "serde", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397406cf04b11ca2a48e6f81804c70af3f40a36abf648e11dc7416043eb0834d" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ark-bn254" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" +dependencies = [ + "ark-ec", + "ark-ff 0.4.2", + "ark-std 0.4.0", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff 0.4.2", + "ark-poly", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin-io" +version = "0.1.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11301df0b06f22dea7bb1916403fdd88a371031e495c49b8f96931b28189e175" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "c-kzg" +version = "2.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "const-hex" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "serde", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.6", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "futures-utils-wasm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2", + "signature", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "keccak-asm" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1766b89733097006f3a1388a02849865d6bc98c89273cb622e29fdd209922183" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "light-poseidon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9a85a9752c549ceb7578064b4ed891179d20acd85f27318573b64d2d7ee7ee" +dependencies = [ + "ark-bn254", + "ark-ff 0.4.2", + "num-bigint", + "thiserror 1.0.69", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "macro-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "nybbles" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" +dependencies = [ + "alloy-rlp", + "cfg-if", + "proptest", + "ruint", + "serde", + "smallvec", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "private-payment-shielded-pool-extension" +version = "0.1.0" +dependencies = [ + "alloy", + "alloy-rlp", + "alloy-trie", + "ark-bn254", + "ark-ff 0.4.2", + "chacha20poly1305", + "hex", + "hkdf", + "k256", + "light-poseidon", + "rand 0.8.6", + "serde", + "serde_json", + "sha2", + "simplepir", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-test", + "toml", + "tracing", + "zeroize", + "zk-kit-lean-imt", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", + "serde", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", + "serde", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "ruint" +version = "1.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.6", + "rand 0.9.4", + "rlp", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.28", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "secp256k1-sys 0.10.1", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.4", + "secp256k1-sys 0.11.0", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + +[[package]] +name = "sha3-asm" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3f15d4e239ebe08413eed880e0f9b5af4b40ee0472543320efa91d488e96a7" +dependencies = [ + "cc", + "cfg-if", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simplepir" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18ca22e3623d85c5bf3d522e81c6426d2efe450e7fd7c8ad89008b4d265b390e" +dependencies = [ + "rand 0.8.6", + "rand_chacha 0.3.1", + "rand_distr", + "thiserror 1.0.69", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-solidity" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec005042c7d952febc1a3ef5b0f6674e9054aa836877a31c90b20e25b3d31744" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver 1.0.28", +] + +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver 1.0.28", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zk-kit-lean-imt" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f0c91d9944d156160791b6b30631f2bfa504d1bc0252a065a5d5d9e1f341693" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/pocs/private-payment/shielded-pool-extension/Cargo.toml b/pocs/private-payment/shielded-pool-extension/Cargo.toml new file mode 100644 index 0000000..db83db6 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "private-payment-shielded-pool-extension" +version = "0.1.0" +edition = "2024" + +[lib] +path = "src/lib/lib.rs" + +[dependencies] +tokio = { version = "1.49", features = ["full", "process"] } +alloy = { version = "1.1", features = ["node-bindings", "sol-types", "essentials"] } +ark-ff = "0.4.2" +ark-bn254 = "0.4" +light-poseidon = "0.2" +zk-kit-lean-imt = { version = "0.1.1", features = ["stateless"] } +k256 = { version = "0.13", features = ["ecdh"] } +chacha20poly1305 = "0.10" +sha2 = "0.10" +hkdf = "0.12" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +toml = "0.8" +hex = "0.4" +thiserror = "2.0" +rand = "0.8.5" +tracing = "0.1" +simplepir = "1.0.1" +alloy-trie = "0.9" +alloy-rlp = "0.3" +zeroize = "1" + +[dev-dependencies] +tokio-test = "0.4" +tempfile = "3.0" + +# SimplePIR uses intentional wrapping u64 arithmetic (built for release); disable +# overflow-checks for just that dependency so it works under the debug/test +# profile too, without relaxing overflow safety in our own crate. +[profile.dev.package.simplepir] +overflow-checks = false diff --git a/pocs/private-payment/shielded-pool-extension/Nargo.toml b/pocs/private-payment/shielded-pool-extension/Nargo.toml new file mode 100644 index 0000000..48fbf26 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/Nargo.toml @@ -0,0 +1,10 @@ +[workspace] +members = [ + "circuits/deposit", + "circuits/transfer", + "circuits/withdraw", + "circuits/chain_update", + "circuits/insertion", + "circuits/insertion_withdraw", +] +default-member = "circuits/transfer" diff --git a/pocs/private-payment/shielded-pool-extension/README.md b/pocs/private-payment/shielded-pool-extension/README.md index 5ae2b4d..cd23d94 100644 --- a/pocs/private-payment/shielded-pool-extension/README.md +++ b/pocs/private-payment/shielded-pool-extension/README.md @@ -1,6 +1,6 @@ # Shielded Pool Extension: PIR + Epoch Nullifiers -A spec-only PoC that extends the [shielded-pool](../shielded-pool/) private-payments construction with two additions: +Extension of the [shielded-pool](../shielded-pool/) private-payments construction with: - PIR over the wallet's pre-spend tree reads, closing the state-read privacy leak at the indexer/RPC layer. - Epoch-based nullifiers with a per-note recursive chain proof (IVC), bounding the on-chain nullifier set without imposing linear per-spend work as notes age. @@ -9,14 +9,83 @@ The note format, deposit, and attestation flows of the parent are preserved. The ## Status -Specification only. No implementation in this folder. The protocol is proof-system agnostic; see [SPEC.md](./SPEC.md) for the abstract requirements an implementation must meet. +Implemented end-to-end as a research PoC: the extended circuits, the two-proof spend contract, the off-chain stack (state replica, bb prover, SimplePIR commitment-path read, light-client storage-proof verifier), and self-contained on-chain integration tests covering both spend flows — transfer (k=2 insertion) and withdraw (k=1 insertion). See "Implementation shortcuts" below for the deliberate spec-vs-impl divergences. The protocol is proof-system agnostic; see [SPEC.md](./SPEC.md) for the abstract requirements an implementation must meet. This folder is a self-contained PoC (no cross-PoC imports from `../shielded-pool/`); any source mirrored from the parent is duplicated locally per the repo's PoC-independence rule. + +## Layout + +``` +shielded-pool-extension/ +├── SPEC.md protocol specification (primary deliverable) +├── README.md this file +├── Cargo.toml wallet / off-chain Rust workspace +├── Nargo.toml Noir circuit workspace +├── foundry.toml Solidity build config +├── circuits/ +│ ├── deposit/ extended deposit circuit +│ ├── transfer/ extended spend circuit (recursive chain-proof verify) +│ ├── withdraw/ extended spend circuit (recursive chain-proof verify) +│ └── chain_update/ new IVC chain-update circuit +├── contracts/ +│ ├── src/ ShieldedPoolExt, interfaces, verifiers +│ ├── script/ deploy scripts +│ └── test/ forge tests +├── scripts/ +│ └── generate-verifiers.sh +└── src/lib/ Rust wallet, PIR client, prover/channel adapters +``` + +## Prerequisites + +- [Foundry](https://getfoundry.sh/introduction/installation) +- [Nargo](https://noir-lang.org/docs/getting_started/noir_installation) **1.0.0-beta.21** — the chain-update recursion (`bb_proof_verification`) requires it; install with `noirup -v 1.0.0-beta.21` +- [Barretenberg (`bb`)](https://barretenberg.aztec.network/docs/getting_started) **5.0.0-nightly.20260324** — run `bbup` after Nargo (it auto-resolves the matching version) +- Rust toolchain (pinned via `rust-toolchain.toml`) + +## Installation + +```bash +cd pocs/private-payment/shielded-pool-extension +forge soldeer install +cp .env.example .env +``` + +## Build & Test + +```bash +# Contracts +forge build +forge test + +# Circuits +nargo compile --workspace +nargo test --workspace + +# Wallet +cargo test --lib + +# Regenerate Solidity verifiers after circuit changes +chmod +x scripts/generate-verifiers.sh +./scripts/generate-verifiers.sh +``` ## Documents -- [SPEC.md](./SPEC.md): protocol specification +- [SPEC.md](./SPEC.md) — protocol specification - Parent SPEC: [`../shielded-pool/SPEC.md`](../shielded-pool/SPEC.md) - Parent REQUIREMENTS: [`../REQUIREMENTS.md`](../REQUIREMENTS.md) +## Implementation shortcuts + +[SPEC.md](./SPEC.md) describes the full protocol, including the parent's KYC attestation registry and flows (unchanged by this extension). Where this PoC's implementation deliberately diverges from the spec, it is noted here: + +- **KYC attestation is not enforced in the deposit circuit.** This extension *composes with* the parent [shielded-pool](../shielded-pool/) rather than replacing it: it layers the PIR commitment-path read and epoch-nullifier machinery on top of the parent's flows. The deposit here proves commitment well-formedness and the new `epoch_created` binding, but does not re-verify KYC attestation membership — that check is orthogonal to the mechanisms this extension demonstrates and is fully enforced in the parent's deposit circuit. + +- **No on-chain nullifier-uniqueness check (differs from the parent contract).** The parent rejected identical/spent nullifiers with an explicit mapping and `IdenticalNullifiers` guard. This extension keeps no on-chain nullifier set: active-epoch double-spend and in-tx duplicate η are caught inside the insertion proof's sorted-low-leaf step (SPEC "On-Chain State"), so the contract only pins the two proofs to one shared η list. + +- **SimplePIR stands in for the SPEC's InsPIRe.** The SPEC specifies InsPIRe (single-server PIR with *silent* preprocessing). Its only Rust implementation (Google's `private-membership`) is an Ubuntu/AVX-512 benchmark CLI over synthetic databases, not a portable library, so the commitment-path read uses the published `simplepir` crate. SimplePIR gives the same index privacy (the server never learns the queried leaf) but is **not** silent — the client keeps a per-database hint from the offline `setup`. InsPIRe's hint-free profile is the documented production target. + +- **The light-client check ships the verifier, not the light client.** `adapters/light_client.rs` verifies a contract storage slot against a state root via the SPEC's two-level MPT proof (`verify_account_storage`), behind the `RootVerifier` port. In production a Helios light client supplies the **consensus-verified** `state_root` (a `HeliosRootVerifier`); that isn't wired here because Helios needs a beacon chain the in-process `anvil` e2e lacks, so the test trusts an anvil block's `stateRoot`. Other root reads remain over plain RPC pending that wiring. + ## Security disclaimer -Research prototype, not production-ready. Cryptographic assumptions and known shortcuts are documented in the SPEC. +Research prototype, not production-ready. Cryptographic assumptions are documented in [SPEC.md](./SPEC.md); implementation shortcuts are listed above. diff --git a/pocs/private-payment/shielded-pool-extension/circuits/chain_update/Nargo.toml b/pocs/private-payment/shielded-pool-extension/circuits/chain_update/Nargo.toml new file mode 100644 index 0000000..3ae62d8 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/chain_update/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "chain_update" +type = "bin" +authors = [""] +compiler_version = ">=1.0.0" + +[dependencies] +poseidon = { tag = "v0.3.0", git = "https://github.com/noir-lang/poseidon" } +bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v5.0.0-nightly.20260324", directory = "barretenberg/noir/bb_proof_verification" } diff --git a/pocs/private-payment/shielded-pool-extension/circuits/chain_update/src/main.nr b/pocs/private-payment/shielded-pool-extension/circuits/chain_update/src/main.nr new file mode 100644 index 0000000..1b44ae8 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/chain_update/src/main.nr @@ -0,0 +1,149 @@ +use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use poseidon::poseidon::bn254::{hash_1, hash_2, hash_3, hash_5}; + +// Chain-Update Circuit (IVC inner artifact). See ../../SPEC.md "Chain-Update +// Circuit (new)". Produces a per-note ChainProof attesting non-spend from +// `epoch_created` through `epoch_validated_through - 1`, extending it one frozen +// epoch per invocation. The inductive step recursively verifies the prior +// ChainProof; the base case (genesis) skips the recursive verify. + +// Depth of the (indexed) frozen nullifier trees. MUST match the off-chain +// `NULLIFIER_TREE_DEPTH` in src/lib/domain/indexed_merkle.rs. +global NULLIFIER_TREE_DEPTH: u32 = 32; + +// Public-input count of a ChainProof, in declaration order: [commitment, +// epoch_created, epoch_validated_through, accumulator, fixed_vk_hash]. +global CHAIN_PROOF_PUB_LEN: u32 = 5; + +// Sorted-low-leaf non-membership of `nullifier` against an indexed-Merkle `root`. +// Mirrors src/lib/domain/indexed_merkle.rs: leaf = hash_3(value, next_value, +// next_index); nodes = hash_2; path bit false = current node is the left child. +// `next_value == 0` denotes +infinity (the current largest leaf). +fn assert_non_membership( + nullifier: Field, + root: Field, + low_value: Field, + low_next_value: Field, + low_next_index: Field, + path_bits: [bool; NULLIFIER_TREE_DEPTH], + siblings: [Field; NULLIFIER_TREE_DEPTH], +) { + // low_value < nullifier < low_next_value (next_value 0 = +inf). + assert(low_value.lt(nullifier), "non-membership: nullifier <= low_value"); + if low_next_value != 0 { + assert(nullifier.lt(low_next_value), "non-membership: nullifier >= low_next_value"); + } + + // Reconstruct the root from the low leaf and its sibling path. + let mut node = hash_3([low_value, low_next_value, low_next_index]); + for i in 0..NULLIFIER_TREE_DEPTH { + if path_bits[i] { + node = hash_2([siblings[i], node]); + } else { + node = hash_2([node, siblings[i]]); + } + } + assert(node == root, "non-membership: root mismatch"); +} + +fn main( + // Public inputs (the ChainProof). Epochs are field-encoded integers. + commitment: pub Field, + epoch_created: pub Field, + epoch_validated_through: pub Field, + accumulator: pub Field, + // The chain-update circuit's own VK hash, propagated along the chain and + // pinned by the spend circuit (closes the self-recursion loop). + fixed_vk_hash: pub Field, + // Private inputs. + is_base_case: bool, + // Prior ChainProof (recursive witness); ignored in the base case. + prior_vk: UltraHonkVerificationKey, + prior_proof: UltraHonkZKProof, + prior_public_inputs: [Field; CHAIN_PROOF_PUB_LEN], + // frozenNullifierRoots[epoch_validated_through_prev]. + frozen_root_next: Field, + // Key + note preimage (commitment and epoch_created are public). + spending_key: Field, + token: Field, + amount: Field, + salt: Field, + // Sorted-low-leaf non-membership witness for the phantom nullifier. + low_value: Field, + low_next_value: Field, + low_next_index: Field, + path_bits: [bool; NULLIFIER_TREE_DEPTH], + siblings: [Field; NULLIFIER_TREE_DEPTH], +) { + if is_base_case { + // Genesis: no prior proof, nothing folded yet. + assert(epoch_validated_through == epoch_created, "base: evt != epoch_created"); + assert(accumulator == 0, "base: accumulator != 0"); + } else { + // 1. Recursively verify the prior ChainProof under the propagated VK hash. + verify_honk_proof(prior_vk, prior_proof, prior_public_inputs, fixed_vk_hash); + + // Prior public inputs (same declaration order as this circuit's). + let prior_commitment = prior_public_inputs[0]; + let prior_epoch_created = prior_public_inputs[1]; + let evt_prev = prior_public_inputs[2]; + let acc_prev = prior_public_inputs[3]; + let prior_vk_hash = prior_public_inputs[4]; + + // 2. The prior proof must be about the same note, under the same VK. + assert(prior_commitment == commitment, "step: commitment mismatch"); + assert(prior_epoch_created == epoch_created, "step: epoch_created mismatch"); + assert(prior_vk_hash == fixed_vk_hash, "step: vk_hash mismatch"); + + // 3. Advance one frozen epoch and fold its root into the accumulator. + assert(epoch_validated_through == evt_prev + 1, "step: evt must advance by 1"); + assert(accumulator == hash_2([acc_prev, frozen_root_next]), "step: accumulator fold"); + + // 4. Key binding: pin spending_key to the public commitment so the + // phantom nullifier below is the one that would really be published. + let owner_pubkey = hash_1([spending_key]); + assert( + commitment == hash_5([token, amount, owner_pubkey, salt, epoch_created]), + "step: commitment preimage mismatch", + ); + + // 5. Phantom nullifier for the just-folded epoch and its non-membership. + let nullifier = hash_3([commitment, spending_key, evt_prev]); + assert_non_membership( + nullifier, + frozen_root_next, + low_value, + low_next_value, + low_next_index, + path_bits, + siblings, + ); + } +} + +// Inductive (recursive) cases require real bb proofs and are exercised by the +// bb-prover harness, not Nargo unit tests. These cover the base-case branch. + +#[test] +fn test_base_case_valid() { + main( + 7, 3, 3, 0, 99, true, [0; 115], [0; 458], [0; 5], 0, 0, 0, 0, 0, 0, 0, 0, [false; 32], + [0; 32], + ); +} + +#[test(should_fail_with = "base: accumulator != 0")] +fn test_base_case_nonzero_accumulator() { + main( + 7, 3, 3, 1, 99, true, [0; 115], [0; 458], [0; 5], 0, 0, 0, 0, 0, 0, 0, 0, [false; 32], + [0; 32], + ); +} + +#[test(should_fail_with = "base: evt != epoch_created")] +fn test_base_case_advanced_epoch() { + main( + 7, 3, 4, 0, 99, true, [0; 115], [0; 458], [0; 5], 0, 0, 0, 0, 0, 0, 0, 0, [false; 32], + [0; 32], + ); +} diff --git a/pocs/private-payment/shielded-pool-extension/circuits/deposit/Nargo.toml b/pocs/private-payment/shielded-pool-extension/circuits/deposit/Nargo.toml new file mode 100644 index 0000000..4eb65f9 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/deposit/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "deposit" +type = "bin" +authors = [""] +compiler_version = ">=1.0.0" + +[dependencies] +poseidon = { tag = "v0.3.0", git = "https://github.com/noir-lang/poseidon" } diff --git a/pocs/private-payment/shielded-pool-extension/circuits/deposit/src/main.nr b/pocs/private-payment/shielded-pool-extension/circuits/deposit/src/main.nr new file mode 100644 index 0000000..d3f3b4c --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/deposit/src/main.nr @@ -0,0 +1,108 @@ +use poseidon::poseidon::bn254::{hash_1, hash_5}; + +// Poseidon wrapper. Arity MUST match the Rust `crypto::poseidon::poseidon5`; +// see `test_poseidon5_matches_rust`. +fn poseidon5(a: Field, b: Field, c: Field, d: Field, e: Field) -> Field { + hash_5([a, b, c, d, e]) +} + +/// Deposit Circuit (extended) +/// +/// Proves the commitment is correctly formed from the note components, binding +/// `epoch_created`. `epoch_created` is forced equal to the public +/// `current_epoch_at_deposit` (which the contract sets to its `currentEpoch`) +/// by feeding it directly into the commitment preimage, so a deposit always +/// mints a note tagged with the live epoch. +/// +/// Implementation shortcut: the parent's KYC attestation-membership check is +/// intentionally omitted from this circuit. The spec still describes it +/// (unchanged from parent); this is a spec-to-implementation divergence +/// documented in README.md "Implementation shortcuts". +/// +/// Public Inputs: +/// - commitment: the note commitment being created +/// - token: ERC-20 token address (lower 160 bits) +/// - amount: deposit amount +/// - current_epoch_at_deposit: contract's `currentEpoch`; bound into the +/// commitment as `epoch_created` (the contract checks it equals `currentEpoch`) +/// +/// Private Inputs: +/// - owner_pubkey: depositor's spending public key (poseidon hash of spending_key) +/// - salt: random value for hiding +fn main( + // Public inputs + commitment: pub Field, + token: pub Field, + amount: pub Field, + current_epoch_at_deposit: pub u64, + // Private inputs + owner_pubkey: Field, + salt: Field, +) { + // Verify commitment is correctly formed and binds the deposit epoch. + // commitment = poseidon5(token, amount, owner_pubkey, salt, epoch_created) + // with epoch_created == current_epoch_at_deposit. + let computed_commitment = + poseidon5(token, amount, owner_pubkey, salt, current_epoch_at_deposit as Field); + assert(commitment == computed_commitment); +} + +#[test] +fn test_deposit_valid() { + let token = 0x1234567890abcdef1234567890abcdef12345678; + let amount = 1000; + let owner_pubkey = hash_1([0xdeadbeef]); // simulated pubkey derivation + let salt = 0xabcd1234; + let current_epoch: u64 = 0; + + let commitment = poseidon5(token, amount, owner_pubkey, salt, current_epoch as Field); + main(commitment, token, amount, current_epoch, owner_pubkey, salt); +} + +#[test] +fn test_deposit_nonzero_epoch() { + // A note deposited after several rollovers binds the live epoch. + let token = 0x1234567890abcdef1234567890abcdef12345678; + let amount = 500; + let owner_pubkey = hash_1([0xbeef]); + let salt = 0x5678; + let current_epoch: u64 = 7; + + let commitment = poseidon5(token, amount, owner_pubkey, salt, current_epoch as Field); + main(commitment, token, amount, current_epoch, owner_pubkey, salt); +} + +#[test(should_fail)] +fn test_deposit_invalid_commitment() { + // Wrong commitment (different salt) must fail. + let token = 0x1234; + let amount = 1000; + let owner_pubkey = hash_1([0xbeef]); + let salt = 0xabcd; + let current_epoch: u64 = 0; + + let wrong_commitment = poseidon5(token, amount, owner_pubkey, 0x9999, current_epoch as Field); + main(wrong_commitment, token, amount, current_epoch, owner_pubkey, salt); +} + +#[test(should_fail)] +fn test_deposit_wrong_epoch() { + // Commitment binds epoch 0 but the contract supplies currentEpoch = 1: + // the commitment check must fail, enforcing epoch_created == currentEpoch. + let token = 0x1234; + let amount = 1000; + let owner_pubkey = hash_1([0xbeef]); + let salt = 0xabcd; + + let commitment = poseidon5(token, amount, owner_pubkey, salt, 0); + main(commitment, token, amount, 1, owner_pubkey, salt); +} + +#[test] +fn test_poseidon5_matches_rust() { + // Cross-implementation vector: MUST equal the Rust crypto::poseidon::poseidon5 + // output for inputs [1,2,3,4,5] (POSEIDON5_12345 in src/lib/crypto/poseidon.rs). + // This pins the off-chain commitment derivation to the in-circuit one. + let expected = 0x0dab9449e4a1398a15224c0b15a49d598b2174d305a316c918125f8feeb123c0; + assert(hash_5([1, 2, 3, 4, 5]) == expected); +} diff --git a/pocs/private-payment/shielded-pool-extension/circuits/insertion/Nargo.toml b/pocs/private-payment/shielded-pool-extension/circuits/insertion/Nargo.toml new file mode 100644 index 0000000..3f8b9a5 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/insertion/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "insertion" +type = "bin" +authors = [""] +compiler_version = ">=1.0.0" + +[dependencies] +insertion_core = { path = "../insertion_core" } diff --git a/pocs/private-payment/shielded-pool-extension/circuits/insertion/src/main.nr b/pocs/private-payment/shielded-pool-extension/circuits/insertion/src/main.nr new file mode 100644 index 0000000..14d5a0d --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/insertion/src/main.nr @@ -0,0 +1,34 @@ +use insertion_core::{NULLIFIER_TREE_DEPTH, run_insertions}; + +// Insertion circuit (relayer-produced; verified on-chain; need not be zk). +// Instantiates the shared `insertion_core::run_insertions` at N_INSERTS = 2 (a +// 2-in transfer). The single-input withdraw uses the same core at N_INSERTS = 1 +// (see `circuits/insertion_withdraw`). See ../../SPEC.md "Insertion Circuit (new)". + +global N_INSERTS: u32 = 2; + +fn main( + pre_active_root: pub Field, + post_active_root: pub Field, + pre_leaf_count: pub u32, + nullifiers: pub [Field; N_INSERTS], + low_value: [Field; N_INSERTS], + low_next_value: [Field; N_INSERTS], + low_next_index: [Field; N_INSERTS], + low_path_bits: [[bool; NULLIFIER_TREE_DEPTH]; N_INSERTS], + low_siblings: [[Field; NULLIFIER_TREE_DEPTH]; N_INSERTS], + new_siblings: [[Field; NULLIFIER_TREE_DEPTH]; N_INSERTS], +) { + run_insertions( + pre_active_root, + post_active_root, + pre_leaf_count, + nullifiers, + low_value, + low_next_value, + low_next_index, + low_path_bits, + low_siblings, + new_siblings, + ); +} diff --git a/pocs/private-payment/shielded-pool-extension/circuits/insertion_core/Nargo.toml b/pocs/private-payment/shielded-pool-extension/circuits/insertion_core/Nargo.toml new file mode 100644 index 0000000..3898167 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/insertion_core/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "insertion_core" +type = "lib" +authors = [""] +compiler_version = ">=1.0.0" + +[dependencies] +poseidon = { tag = "v0.3.0", git = "https://github.com/noir-lang/poseidon" } diff --git a/pocs/private-payment/shielded-pool-extension/circuits/insertion_core/src/lib.nr b/pocs/private-payment/shielded-pool-extension/circuits/insertion_core/src/lib.nr new file mode 100644 index 0000000..a8f00c2 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/insertion_core/src/lib.nr @@ -0,0 +1,101 @@ +use poseidon::poseidon::bn254::{hash_2, hash_3}; + +// Shared insertion logic for the relayer's insertion proof, generic over the +// number of insertions `N`. Instantiated at N = 2 (a 2-in transfer) by the +// `insertion` circuit and at N = 1 (a 1-in withdraw) by `insertion_withdraw`; +// the logic is identical, only the insertion count differs. The in-circuit +// mirror of the off-chain `InsertionWitness::verify_and_apply` +// (src/lib/domain/indexed_merkle.rs). See ../../SPEC.md "Insertion Circuit (new)". + +pub global NULLIFIER_TREE_DEPTH: u32 = 32; + +fn poseidon2(a: Field, b: Field) -> Field { + hash_2([a, b]) +} + +// leaf = hash_3(value, next_value, next_index) - matches the off-chain IMT. +fn leaf_hash(value: Field, next_value: Field, next_index: Field) -> Field { + hash_3([value, next_value, next_index]) +} + +// Reconstruct the root from a leaf and a sibling path; bit false = left child. +fn root_from_path( + leaf: Field, + bits: [bool; NULLIFIER_TREE_DEPTH], + siblings: [Field; NULLIFIER_TREE_DEPTH], +) -> Field { + let mut node = leaf; + for i in 0..NULLIFIER_TREE_DEPTH { + if bits[i] { + node = poseidon2(siblings[i], node); + } else { + node = poseidon2(node, siblings[i]); + } + } + node +} + +// Little-endian bit decomposition of a tree index (binds the new leaf to its +// canonical append slot). +fn index_bits(index: u32) -> [bool; NULLIFIER_TREE_DEPTH] { + let mut bits: [bool; NULLIFIER_TREE_DEPTH] = [false; NULLIFIER_TREE_DEPTH]; + for j in 0..NULLIFIER_TREE_DEPTH { + bits[j] = ((index >> j) & 1) == 1; + } + bits +} + +// Chain `N` sorted-low-leaf insertions of the public `nullifiers` list into the +// active indexed Merkle tree, threading `pre_active_root` to `post_active_root`. +pub fn run_insertions( + pre_active_root: Field, + post_active_root: Field, + pre_leaf_count: u32, + nullifiers: [Field; N], + // Per insertion: predecessor (low) leaf + its sibling path, and the sibling + // path of the (empty) append slot. + low_value: [Field; N], + low_next_value: [Field; N], + low_next_index: [Field; N], + low_path_bits: [[bool; NULLIFIER_TREE_DEPTH]; N], + low_siblings: [[Field; NULLIFIER_TREE_DEPTH]; N], + new_siblings: [[Field; NULLIFIER_TREE_DEPTH]; N], +) { + let empty = leaf_hash(0, 0, 0); + let mut root = pre_active_root; + + for i in 0..N { + let nullifier = nullifiers[i]; + // Canonical append index for this insertion. + let new_index = pre_leaf_count + i; + let new_bits = index_bits(new_index); + + // 1. Predecessor membership in the current root + non-membership of the + // nullifier (low.value < nullifier < low.next_value; next_value 0 = inf). + let low = leaf_hash(low_value[i], low_next_value[i], low_next_index[i]); + assert( + root_from_path(low, low_path_bits[i], low_siblings[i]) == root, + "insertion: predecessor membership", + ); + assert(low_value[i].lt(nullifier), "insertion: nullifier <= low_value"); + if low_next_value[i] != 0 { + assert(nullifier.lt(low_next_value[i]), "insertion: nullifier >= low_next_value"); + } + + // 2. Repoint the predecessor at the new leaf -> intermediate root r'. + let updated_low = leaf_hash(low_value[i], nullifier, new_index as Field); + let r_prime = root_from_path(updated_low, low_path_bits[i], low_siblings[i]); + + // 3. The canonical append slot must be empty in r'. + assert( + root_from_path(empty, new_bits, new_siblings[i]) == r_prime, + "insertion: append slot not empty", + ); + + // 4. Write the new leaf (inherits the predecessor's old forward pointer). + let new_leaf = leaf_hash(nullifier, low_next_value[i], low_next_index[i]); + root = root_from_path(new_leaf, new_bits, new_siblings[i]); + } + + assert(root == post_active_root, "insertion: post-root mismatch"); +} diff --git a/pocs/private-payment/shielded-pool-extension/circuits/insertion_withdraw/Nargo.toml b/pocs/private-payment/shielded-pool-extension/circuits/insertion_withdraw/Nargo.toml new file mode 100644 index 0000000..6d49511 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/insertion_withdraw/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "insertion_withdraw" +type = "bin" +authors = [""] +compiler_version = ">=1.0.0" + +[dependencies] +insertion_core = { path = "../insertion_core" } diff --git a/pocs/private-payment/shielded-pool-extension/circuits/insertion_withdraw/src/main.nr b/pocs/private-payment/shielded-pool-extension/circuits/insertion_withdraw/src/main.nr new file mode 100644 index 0000000..df61a0d --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/insertion_withdraw/src/main.nr @@ -0,0 +1,35 @@ +use insertion_core::{NULLIFIER_TREE_DEPTH, run_insertions}; + +// Withdraw's insertion circuit: the shared `insertion_core::run_insertions` at +// N_INSERTS = 1 (a single-input withdraw inserts one nullifier). Identical logic +// to `circuits/insertion` (k = 2); only the insertion count differs. The +// contract's `withdrawInsertionVerifier` consumes this circuit's verifier. See +// ../../SPEC.md "Insertion Circuit (new)" and "Withdraw (extended)". + +global N_INSERTS: u32 = 1; + +fn main( + pre_active_root: pub Field, + post_active_root: pub Field, + pre_leaf_count: pub u32, + nullifiers: pub [Field; N_INSERTS], + low_value: [Field; N_INSERTS], + low_next_value: [Field; N_INSERTS], + low_next_index: [Field; N_INSERTS], + low_path_bits: [[bool; NULLIFIER_TREE_DEPTH]; N_INSERTS], + low_siblings: [[Field; NULLIFIER_TREE_DEPTH]; N_INSERTS], + new_siblings: [[Field; NULLIFIER_TREE_DEPTH]; N_INSERTS], +) { + run_insertions( + pre_active_root, + post_active_root, + pre_leaf_count, + nullifiers, + low_value, + low_next_value, + low_next_index, + low_path_bits, + low_siblings, + new_siblings, + ); +} diff --git a/pocs/private-payment/shielded-pool-extension/circuits/transfer/Nargo.toml b/pocs/private-payment/shielded-pool-extension/circuits/transfer/Nargo.toml new file mode 100644 index 0000000..787f06c --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/transfer/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "transfer" +type = "bin" +authors = [""] +compiler_version = ">=1.0.0" + +[dependencies] +poseidon = { tag = "v0.3.0", git = "https://github.com/noir-lang/poseidon" } +bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v5.0.0-nightly.20260324", directory = "barretenberg/noir/bb_proof_verification" } diff --git a/pocs/private-payment/shielded-pool-extension/circuits/transfer/src/main.nr b/pocs/private-payment/shielded-pool-extension/circuits/transfer/src/main.nr new file mode 100644 index 0000000..d078956 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/transfer/src/main.nr @@ -0,0 +1,203 @@ +use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use poseidon::poseidon::bn254::{hash_1, hash_2, hash_3, hash_5}; + +// Transfer spend circuit (extended, 2-in-2-out). See ../../SPEC.md "Spend +// Circuit (Transfer / Withdraw, diff)". +// +// Per input: reconstruct the commitment binding `epoch_created` (parent key +// binding owner_pubkey = poseidon(spending_key), now poseidon5), verify +// commitment-tree membership, recursively verify the note's ChainProof, and emit +// the per-epoch active nullifier `poseidon3(commitment, sk, current_epoch)`. +// Outputs are minted at `current_epoch`. Performs NO active-tree work (that is +// the relayer's insertion circuit). Value preservation + token consistency are +// inherited from the parent. + +global MAX_COMMITMENT_TREE_DEPTH: u32 = 32; +global CHAIN_PROOF_PUB_LEN: u32 = 5; + +fn poseidon1(a: Field) -> Field { + hash_1([a]) +} +fn poseidon2(a: Field, b: Field) -> Field { + hash_2([a, b]) +} +fn poseidon3(a: Field, b: Field, c: Field) -> Field { + hash_3([a, b, c]) +} +fn poseidon5(a: Field, b: Field, c: Field, d: Field, e: Field) -> Field { + hash_5([a, b, c, d, e]) +} + +// LeanIMT commitment-tree membership (dynamic depth). Mirrors the off-chain +// `CommitmentMerkleProof::reconstruct_root`: fold the leaf up `proof_length` +// levels with poseidon2; `indices[i] == 0` => current node is the left child. +fn verify_membership( + leaf: Field, + root: Field, + proof_length: u32, + indices: [bool; MAX_COMMITMENT_TREE_DEPTH], + path: [Field; MAX_COMMITMENT_TREE_DEPTH], +) { + let mut node = leaf; + for i in 0..MAX_COMMITMENT_TREE_DEPTH { + if i < proof_length { + if indices[i] { + node = poseidon2(path[i], node); + } else { + node = poseidon2(node, path[i]); + } + } + } + assert(node == root, "membership: root mismatch"); +} + +// Recursively verify one input note's ChainProof and bind its public inputs to +// the spend's per-input values. The chain proof must be fully caught up +// (`epoch_validated_through == current_epoch`). +fn verify_input_chain( + chain_vk: UltraHonkVerificationKey, + chain_proof: UltraHonkZKProof, + chain_pub: [Field; CHAIN_PROOF_PUB_LEN], + chain_vk_hash: Field, + commitment_in: Field, + epoch_created_in: Field, + current_epoch: Field, + chain_accumulator_in: Field, +) { + verify_honk_proof(chain_vk, chain_proof, chain_pub, chain_vk_hash); + assert(chain_pub[0] == commitment_in, "chain: commitment mismatch"); + assert(chain_pub[1] == epoch_created_in, "chain: epoch_created mismatch"); + assert(chain_pub[2] == current_epoch, "chain: not caught up to current_epoch"); + assert(chain_pub[3] == chain_accumulator_in, "chain: accumulator mismatch"); + assert(chain_pub[4] == chain_vk_hash, "chain: vk_hash mismatch"); +} + +fn main( + // Public inputs. Epochs are field-encoded integers. + nullifier_active_0: pub Field, + nullifier_active_1: pub Field, + commitment_out_0: pub Field, + commitment_out_1: pub Field, + commitment_root: pub Field, + current_epoch: pub Field, + // Chain-update circuit's VK hash; the contract pins it to its deploy-time value. + chain_vk_hash: pub Field, + epoch_created_in_0: pub Field, + epoch_created_in_1: pub Field, + chain_accumulator_in_0: pub Field, + chain_accumulator_in_1: pub Field, + // Private inputs. + spending_key: Field, + token_in_0: Field, + amount_in_0: u64, + salt_in_0: Field, + token_in_1: Field, + amount_in_1: u64, + salt_in_1: Field, + token_out_0: Field, + amount_out_0: u64, + owner_out_0: Field, + salt_out_0: Field, + token_out_1: Field, + amount_out_1: u64, + owner_out_1: Field, + salt_out_1: Field, + proof_length: u32, + path_0: [Field; MAX_COMMITMENT_TREE_DEPTH], + indices_0: [bool; MAX_COMMITMENT_TREE_DEPTH], + path_1: [Field; MAX_COMMITMENT_TREE_DEPTH], + indices_1: [bool; MAX_COMMITMENT_TREE_DEPTH], + // One shared chain-update VK; one ChainProof per input note. + chain_vk: UltraHonkVerificationKey, + chain_proof_0: UltraHonkZKProof, + chain_pub_0: [Field; CHAIN_PROOF_PUB_LEN], + chain_proof_1: UltraHonkZKProof, + chain_pub_1: [Field; CHAIN_PROOF_PUB_LEN], +) { + // Parent constraint 1: owner_pubkey is derived from the spending key, so a + // valid spend proves ownership of the input notes. + let owner_pubkey = poseidon1(spending_key); + + // --- Input 0 (always a real note) --- + let commitment_in_0 = + poseidon5(token_in_0, amount_in_0 as Field, owner_pubkey, salt_in_0, epoch_created_in_0); + verify_membership(commitment_in_0, commitment_root, proof_length, indices_0, path_0); + assert( + nullifier_active_0 == poseidon3(commitment_in_0, spending_key, current_epoch), + "nullifier_0 mismatch", + ); + verify_input_chain( + chain_vk, + chain_proof_0, + chain_pub_0, + chain_vk_hash, + commitment_in_0, + epoch_created_in_0, + current_epoch, + chain_accumulator_in_0, + ); + + // --- Input 1 (may be a zero/padding note: amount 0, not in the tree) --- + let commitment_in_1 = + poseidon5(token_in_1, amount_in_1 as Field, owner_pubkey, salt_in_1, epoch_created_in_1); + let is_zero_note = amount_in_1 == 0; + if !is_zero_note { + verify_membership(commitment_in_1, commitment_root, proof_length, indices_1, path_1); + verify_input_chain( + chain_vk, + chain_proof_1, + chain_pub_1, + chain_vk_hash, + commitment_in_1, + epoch_created_in_1, + current_epoch, + chain_accumulator_in_1, + ); + } + // The nullifier is constrained even for a zero note (cannot be forged). + assert( + nullifier_active_1 == poseidon3(commitment_in_1, spending_key, current_epoch), + "nullifier_1 mismatch", + ); + + // --- Outputs (minted with epoch_created == current_epoch) --- + assert( + commitment_out_0 + == poseidon5(token_out_0, amount_out_0 as Field, owner_out_0, salt_out_0, current_epoch), + "commitment_out_0 mismatch", + ); + assert( + commitment_out_1 + == poseidon5(token_out_1, amount_out_1 as Field, owner_out_1, salt_out_1, current_epoch), + "commitment_out_1 mismatch", + ); + + // --- Value preservation + token consistency (parent) --- + assert(amount_in_0 + amount_in_1 == amount_out_0 + amount_out_1, "value not conserved"); + assert(token_in_0 == token_in_1, "input token mismatch"); + assert(token_in_0 == token_out_0, "output_0 token mismatch"); + assert(token_in_0 == token_out_1, "output_1 token mismatch"); +} + +// Recursive (full-spend) cases need real bb chain proofs and are exercised by +// the bb-prover harness / e2e. This unit test covers the non-recursive +// commitment-tree membership helper. +#[test] +fn test_membership_roundtrip() { + let leaf = 0x1111; + let sibling = 0x2222; + let mut path: [Field; MAX_COMMITMENT_TREE_DEPTH] = [0; MAX_COMMITMENT_TREE_DEPTH]; + let indices: [bool; MAX_COMMITMENT_TREE_DEPTH] = [false; MAX_COMMITMENT_TREE_DEPTH]; + path[0] = sibling; + let root = poseidon2(leaf, sibling); // leaf at index 0, depth 1 + verify_membership(leaf, root, 1, indices, path); +} + +#[test(should_fail_with = "membership: root mismatch")] +fn test_membership_wrong_root() { + let leaf = 0x1111; + let mut path: [Field; MAX_COMMITMENT_TREE_DEPTH] = [0; MAX_COMMITMENT_TREE_DEPTH]; + let indices: [bool; MAX_COMMITMENT_TREE_DEPTH] = [false; MAX_COMMITMENT_TREE_DEPTH]; + path[0] = 0x2222; + verify_membership(leaf, 0x9999, 1, indices, path); +} diff --git a/pocs/private-payment/shielded-pool-extension/circuits/withdraw/Nargo.toml b/pocs/private-payment/shielded-pool-extension/circuits/withdraw/Nargo.toml new file mode 100644 index 0000000..67b1862 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/withdraw/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "withdraw" +type = "bin" +authors = [""] +compiler_version = ">=1.0.0" + +[dependencies] +poseidon = { tag = "v0.3.0", git = "https://github.com/noir-lang/poseidon" } +bb_proof_verification = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v5.0.0-nightly.20260324", directory = "barretenberg/noir/bb_proof_verification" } diff --git a/pocs/private-payment/shielded-pool-extension/circuits/withdraw/src/main.nr b/pocs/private-payment/shielded-pool-extension/circuits/withdraw/src/main.nr new file mode 100644 index 0000000..18e7887 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/circuits/withdraw/src/main.nr @@ -0,0 +1,140 @@ +use bb_proof_verification::{UltraHonkVerificationKey, UltraHonkZKProof, verify_honk_proof}; +use poseidon::poseidon::bn254::{hash_1, hash_2, hash_3, hash_5}; + +// Withdraw spend circuit (extended). Same per-input diff as `transfer`, applied +// to a single input note (full withdrawal, no change output): reconstruct the +// commitment binding `epoch_created`, verify commitment-tree membership, +// recursively verify the note's ChainProof, and emit the per-epoch active +// nullifier. The note's full `amount` exits the pool to `recipient`. See +// ../../SPEC.md "Withdraw (extended)". + +global MAX_COMMITMENT_TREE_DEPTH: u32 = 32; +global CHAIN_PROOF_PUB_LEN: u32 = 5; + +fn poseidon1(a: Field) -> Field { + hash_1([a]) +} +fn poseidon2(a: Field, b: Field) -> Field { + hash_2([a, b]) +} +fn poseidon3(a: Field, b: Field, c: Field) -> Field { + hash_3([a, b, c]) +} +fn poseidon5(a: Field, b: Field, c: Field, d: Field, e: Field) -> Field { + hash_5([a, b, c, d, e]) +} + +// LeanIMT commitment-tree membership (dynamic depth); mirrors the off-chain +// `CommitmentMerkleProof::reconstruct_root`. +fn verify_membership( + leaf: Field, + root: Field, + proof_length: u32, + indices: [bool; MAX_COMMITMENT_TREE_DEPTH], + path: [Field; MAX_COMMITMENT_TREE_DEPTH], +) { + let mut node = leaf; + for i in 0..MAX_COMMITMENT_TREE_DEPTH { + if i < proof_length { + if indices[i] { + node = poseidon2(path[i], node); + } else { + node = poseidon2(node, path[i]); + } + } + } + assert(node == root, "membership: root mismatch"); +} + +// Recursively verify the input note's ChainProof, fully caught up to the +// current epoch, and bind its public inputs. +fn verify_input_chain( + chain_vk: UltraHonkVerificationKey, + chain_proof: UltraHonkZKProof, + chain_pub: [Field; CHAIN_PROOF_PUB_LEN], + chain_vk_hash: Field, + commitment_in: Field, + epoch_created_in: Field, + current_epoch: Field, + chain_accumulator_in: Field, +) { + verify_honk_proof(chain_vk, chain_proof, chain_pub, chain_vk_hash); + assert(chain_pub[0] == commitment_in, "chain: commitment mismatch"); + assert(chain_pub[1] == epoch_created_in, "chain: epoch_created mismatch"); + assert(chain_pub[2] == current_epoch, "chain: not caught up to current_epoch"); + assert(chain_pub[3] == chain_accumulator_in, "chain: accumulator mismatch"); + assert(chain_pub[4] == chain_vk_hash, "chain: vk_hash mismatch"); +} + +fn main( + // Public inputs. Epochs are field-encoded integers. + nullifier_active: pub Field, + token: pub Field, + amount: pub Field, + recipient: pub Field, + commitment_root: pub Field, + current_epoch: pub Field, + chain_vk_hash: pub Field, + epoch_created_in: pub Field, + chain_accumulator_in: pub Field, + // Private inputs. + spending_key: Field, + salt: Field, + proof_length: u32, + path: [Field; MAX_COMMITMENT_TREE_DEPTH], + indices: [bool; MAX_COMMITMENT_TREE_DEPTH], + chain_vk: UltraHonkVerificationKey, + chain_proof: UltraHonkZKProof, + chain_pub: [Field; CHAIN_PROOF_PUB_LEN], +) { + // Parent constraint 1: owner_pubkey derived from the spending key. + let owner_pubkey = poseidon1(spending_key); + + // Reconstruct the note commitment binding epoch_created (the claimed + // token/amount must match the committed note). + let commitment_in = poseidon5(token, amount, owner_pubkey, salt, epoch_created_in); + + verify_membership(commitment_in, commitment_root, proof_length, indices, path); + + assert( + nullifier_active == poseidon3(commitment_in, spending_key, current_epoch), + "nullifier mismatch", + ); + + verify_input_chain( + chain_vk, + chain_proof, + chain_pub, + chain_vk_hash, + commitment_in, + epoch_created_in, + current_epoch, + chain_accumulator_in, + ); + + // `recipient` is bound to the proof via the public inputs; the contract + // sends the withdrawn funds there. No further in-circuit constraint. + let _ = recipient; +} + +// The full recursive `main` is exercised by the bb-prover harness / e2e. This +// unit test covers the non-recursive membership helper. +#[test] +fn test_membership_roundtrip() { + let leaf = 0x3333; + let sibling = 0x4444; + let mut path: [Field; MAX_COMMITMENT_TREE_DEPTH] = [0; MAX_COMMITMENT_TREE_DEPTH]; + let indices: [bool; MAX_COMMITMENT_TREE_DEPTH] = [false; MAX_COMMITMENT_TREE_DEPTH]; + path[0] = sibling; + let root = poseidon2(leaf, sibling); + verify_membership(leaf, root, 1, indices, path); +} + +#[test(should_fail_with = "membership: root mismatch")] +fn test_membership_wrong_root() { + let leaf = 0x3333; + let mut path: [Field; MAX_COMMITMENT_TREE_DEPTH] = [0; MAX_COMMITMENT_TREE_DEPTH]; + let indices: [bool; MAX_COMMITMENT_TREE_DEPTH] = [false; MAX_COMMITMENT_TREE_DEPTH]; + path[0] = 0x4444; + verify_membership(leaf, 0x9999, 1, indices, path); +} diff --git a/pocs/private-payment/shielded-pool-extension/contracts/script/Deploy.s.sol b/pocs/private-payment/shielded-pool-extension/contracts/script/Deploy.s.sol new file mode 100644 index 0000000..8332328 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/contracts/script/Deploy.s.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Script} from "forge-std/src/Script.sol"; +import {Config} from "forge-std/src/Config.sol"; +import {ShieldedPoolExt} from "../src/ShieldedPoolExt.sol"; +import {MockERC20} from "../src/mocks/MockERC20.sol"; +import {HonkVerifier as DepositVerifier} from "../src/verifiers/DepositVerifier.sol"; +import {HonkVerifier as TransferVerifier} from "../src/verifiers/TransferVerifier.sol"; +import {HonkVerifier as WithdrawVerifier} from "../src/verifiers/WithdrawVerifier.sol"; +import {HonkVerifier as InsertionVerifier} from "../src/verifiers/InsertionVerifier.sol"; +import {HonkVerifier as WithdrawInsertionVerifier} from "../src/verifiers/WithdrawInsertionVerifier.sol"; + +/// @notice E2e deployment: real bb verifiers + mock token + `ShieldedPoolExt`. +/// @dev `CHAIN_VK_HASH` (the chain-update circuit's VK hash) and `EMPTY_IMT_ROOT` +/// (the empty indexed-Merkle-tree root) are computed off-chain by the +/// integration test (via `bb` and the Rust IMT) and passed as env vars. +/// Forge auto-deploys + links the contract libraries (ZKTranscriptLib in the +/// verifiers; PoseidonT3 + LeanIMT in the pool). Deployed addresses are +/// recorded to `deployments.toml` through the forge-std `Config` base; the +/// integration test seeds the `[31337]` chain skeleton the base reads on load. +contract Deploy is Script, Config { + function run() external { + _loadConfig("./deployments.toml", true); + + bytes32 chainVkHash = vm.envBytes32("CHAIN_VK_HASH"); + bytes32 emptyImtRoot = vm.envBytes32("EMPTY_IMT_ROOT"); + + vm.startBroadcast(); + address depositV = address(new DepositVerifier()); + address transferV = address(new TransferVerifier()); + address withdrawV = address(new WithdrawVerifier()); + address insertionV = address(new InsertionVerifier()); + // k=1 insertion verifier for the single-input withdraw (transfer uses k=2). + address withdrawInsertionV = address(new WithdrawInsertionVerifier()); + MockERC20 token = new MockERC20("USD Coin", "USDC", 6); + ShieldedPoolExt pool = new ShieldedPoolExt( + depositV, transferV, insertionV, withdrawV, withdrawInsertionV, chainVkHash, emptyImtRoot + ); + vm.stopBroadcast(); + + config.set("shielded_pool", address(pool)); + config.set("mock_token", address(token)); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/contracts/src/ShieldedPoolExt.sol b/pocs/private-payment/shielded-pool-extension/contracts/src/ShieldedPoolExt.sol new file mode 100644 index 0000000..aab5c32 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/contracts/src/ShieldedPoolExt.sol @@ -0,0 +1,442 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin-contracts/token/ERC20/utils/SafeERC20.sol"; +import {IVerifier} from "./interfaces/IVerifier.sol"; +import {LeanIMT, LeanIMTData} from "@zk-kit/packages/lean-imt/contracts/LeanIMT.sol"; +import {PoseidonT3} from "poseidon-solidity/PoseidonT3.sol"; + +/// @title ShieldedPoolExt +/// @notice Privacy-preserving payment pool extended with epoch-based nullifiers +/// and PIR-served reads (research prototype). See ../../SPEC.md. +/// @dev Current scope: deposits, epoch rollover, and the full spend path — +/// `transfer` (2-in-2-out) and `withdraw` (single input). `currentEpoch` +/// advances via `rolloverEpoch()`, freezing each epoch's active-nullifier +/// root. Both spend functions run the two-proof path (wallet spend proof + +/// relayer insertion proof), advancing `activeNullifierRoot`/ +/// `activeLeafCount` via the shared `_verifyInsertionAndAdvance` helper. +contract ShieldedPoolExt { + using SafeERC20 for IERC20; + using LeanIMT for LeanIMTData; + + /// @notice Maximum number of historical commitment roots retained. + uint256 public constant MAX_HISTORICAL_ROOTS = 100; + + /// @notice LeanIMT commitment tree (append-only; membership only). + LeanIMTData internal _tree; + + // Historical commitment-root buffer. Deposits and transfers populate it; it + // is *consumed* at spend time, when `transfer` (and later `withdraw`) pin the + // spend proof's `commitment_root` to a recent root via `isKnownRoot`. + /// @notice Historical commitment roots (circular buffer). + bytes32[100] public historicalRoots; + /// @notice Next write index into the historical-roots buffer. + uint256 public historicalRootIndex; + /// @notice Superseded commitment roots still accepted in proofs. + mapping(bytes32 => bool) public validRoots; + + /// @notice Tokens accepted by the pool. + mapping(address => bool) public supportedTokens; + + /// @notice Verifier for the deposit proof (IVerifier.verify). + IVerifier public depositVerifier; + + /// @notice Verifier for the wallet's transfer spend proof (zero-knowledge). + IVerifier public transferVerifier; + + /// @notice Verifier for the relayer's 2-insertion proof used by `transfer` + /// (advances the active nullifier tree by k=2; not zero-knowledge). + IVerifier public insertionVerifier; + + /// @notice Verifier for the wallet's withdraw spend proof (zero-knowledge). + IVerifier public withdrawVerifier; + + /// @notice Verifier for the relayer's 1-insertion proof used by `withdraw` + /// (advances the active nullifier tree by k=1; not zero-knowledge). + /// Distinct from `insertionVerifier`: the insertion circuit's leaf + /// count is fixed per circuit, so k=1 and k=2 are separate artifacts. + IVerifier public withdrawInsertionVerifier; + + /// @notice Hash of the chain-update circuit's verifying key (SPEC `FixedVK`), + /// fixed at deployment. Supplied as a spend-proof public input so a + /// valid proof is pinned to the legitimate chain-update circuit; a + /// spend recursing over a forged chain circuit cannot verify. + bytes32 public immutable chainVkHash; + + /// @notice Contract owner (PoC: also the epoch-rollover trigger in later slices). + address public owner; + + /// @notice Current epoch, bound into each deposited note's commitment as + /// `epoch_created`. Advanced by `rolloverEpoch()`. + uint64 public currentEpoch; + + /// @notice Frozen active-nullifier-tree root per past epoch `e`. + mapping(uint64 => bytes32) public frozenNullifierRoots; + + /// @notice Root of the current epoch's active nullifier tree. Advanced by the + /// transfer/withdraw spend path; reset to `emptyImtRoot` on rollover. + bytes32 public activeNullifierRoot; + + /// @notice Next free leaf index in the active nullifier tree (canonical append + /// index). Starts at 1: index 0 is the indexed tree's genesis leaf + /// (the bootstrap low-leaf for sorted-low-leaf insertion), so real + /// nullifiers append from index 1. NB: the SPEC's rolloverEpoch + /// pseudocode resets this to 0; the genesis-leaf bootstrap makes 1 the + /// correct value (reconciliation flagged in review). + uint64 public activeLeafCount; + + /// @notice Root of an empty active nullifier tree (genesis-leaf-only tree). + /// The SPEC's `EMPTY_IMT_ROOT`; fixed at deployment. + bytes32 public immutable emptyImtRoot; + + event Deposit(bytes32 indexed commitment, address indexed token, uint256 amount, bytes encryptedNote); + /// @dev `nullifier1`/`nullifier2` are the per-input active nullifiers η; replicas + /// replay them in event order to rebuild the active indexed tree. + event Transfer( + bytes32 indexed nullifier1, + bytes32 indexed nullifier2, + bytes32 commitment1, + bytes32 commitment2, + bytes encryptedNotes + ); + /// @dev `nullifier` is the spent note's active η; replicas replay it (in event + /// order, interleaved with `Transfer`) to rebuild the active indexed tree. + event Withdraw(bytes32 indexed nullifier, address indexed recipient, address indexed token, uint256 amount); + event EpochRollover(uint64 indexed epoch, bytes32 root); + event TokenAdded(address indexed token); + event TokenRemoved(address indexed token); + event DepositVerifierUpdated(address indexed oldVerifier, address indexed newVerifier); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + error OnlyOwner(); + error UnsupportedToken(); + error ZeroAmount(); + error InvalidProof(); + error InvalidRoot(); + error ZeroAddress(); + error TokenAlreadySupported(); + error TokenNotSupported(); + + modifier onlyOwner() { + _onlyOwner(); + _; + } + + function _onlyOwner() internal view { + if (msg.sender != owner) revert OnlyOwner(); + } + + constructor( + address _depositVerifier, + address _transferVerifier, + address _insertionVerifier, + address _withdrawVerifier, + address _withdrawInsertionVerifier, + bytes32 _chainVkHash, + bytes32 _emptyImtRoot + ) { + if (_depositVerifier == address(0)) revert ZeroAddress(); + if (_transferVerifier == address(0)) revert ZeroAddress(); + if (_insertionVerifier == address(0)) revert ZeroAddress(); + if (_withdrawVerifier == address(0)) revert ZeroAddress(); + if (_withdrawInsertionVerifier == address(0)) revert ZeroAddress(); + depositVerifier = IVerifier(_depositVerifier); + transferVerifier = IVerifier(_transferVerifier); + insertionVerifier = IVerifier(_insertionVerifier); + withdrawVerifier = IVerifier(_withdrawVerifier); + withdrawInsertionVerifier = IVerifier(_withdrawInsertionVerifier); + chainVkHash = _chainVkHash; + emptyImtRoot = _emptyImtRoot; + activeNullifierRoot = _emptyImtRoot; + activeLeafCount = 1; // index 0 is the indexed tree's genesis leaf + owner = msg.sender; + emit OwnershipTransferred(address(0), msg.sender); + } + + /// @notice Current commitment-tree root. + function commitmentRoot() public view returns (bytes32) { + return bytes32(_tree.root()); + } + + /// @notice Number of commitments inserted into the tree. + function getCommitmentCount() external view returns (uint256) { + return _tree.size; + } + + /// @notice Deposit tokens, minting a note committed at the current epoch. + /// @dev Deposit-proof public inputs are [commitment, token, amount, currentEpoch]. + /// The contract supplies `currentEpoch` as the final input, so a valid + /// proof enforces `epoch_created == currentEpoch` on the new commitment. + /// @param proof Deposit ZK proof. + /// @param commitment The note commitment (binds `epoch_created`). + /// @param token ERC-20 token address. + /// @param amount Amount to deposit. + /// @param encryptedNote Encrypted note payload for viewing-key holders. + function deposit( + bytes calldata proof, + bytes32 commitment, + address token, + uint256 amount, + bytes calldata encryptedNote + ) external { + if (!supportedTokens[token]) revert UnsupportedToken(); + if (amount == 0) revert ZeroAmount(); + + bytes32[] memory publicInputs = new bytes32[](4); + publicInputs[0] = commitment; + publicInputs[1] = bytes32(uint256(uint160(token))); + publicInputs[2] = bytes32(amount); + publicInputs[3] = bytes32(uint256(currentEpoch)); + + if (!depositVerifier.verify(proof, publicInputs)) revert InvalidProof(); + + _insertCommitment(uint256(commitment)); + IERC20(token).safeTransferFrom(msg.sender, address(this), amount); + + emit Deposit(commitment, token, amount, encryptedNote); + } + + /// @notice Recompute the chain accumulator a spend's per-input chain proof + /// must match: fold `frozenNullifierRoots[e]` for + /// `e in [epochCreated, currentEpoch)` under Poseidon, seeded at 0. + /// @dev Mirrors the chain-update circuit's accumulator fold + /// (`hash_2([acc, frozen_root])`). PoseidonT3 is the same primitive the + /// LeanIMT uses for the commitment tree, so the on-chain and in-circuit + /// hashes agree. Cost is O(currentEpoch - epochCreated); bounded by + /// coarse epochs (SPEC "Epoch Cadence"). A note created in the current + /// epoch yields 0 (the genesis chain proof's accumulator). + function expectedChainAccumulator(uint64 epochCreated) public view returns (bytes32) { + uint256 acc = 0; + for (uint64 e = epochCreated; e < currentEpoch; e++) { + acc = PoseidonT3.hash([acc, uint256(frozenNullifierRoots[e])]); + } + return bytes32(acc); + } + + /// @notice Spend two input notes into two output notes (2-in-2-out), advancing + /// the active nullifier tree via the relayer's insertion proof. + /// @dev Two-proof spend path (SPEC "Private Transfer"). The contract supplies + /// every state-pinned public input, so a valid proof is forced to match + /// contract state: + /// - spend proof: `current_epoch`, `chainVkHash`, and the per-input + /// `expectedChainAccumulator(epochCreated[i])` (realizing the SPEC's + /// `accumulator == expectedChainAccumulator(...)` check by construction); + /// - insertion proof: `pre_active_root = activeNullifierRoot` and + /// `pre_leaf_count = activeLeafCount`. + /// Cross-proof binding (SPEC "Cross-proof binding") is structural: the + /// same `nullifiers` array is fed to both verifiers, so both proofs are + /// pinned to one identical ordered η list — no element-wise comparison + /// needed. Active-epoch double-spend and in-tx duplicate η are caught by + /// the insertion proof's sorted-low-leaf step, so the contract keeps no + /// nullifier set and runs no uniqueness check. + /// @param spendProof Wallet spend proof (commitment membership, chain-proof + /// recursion, nullifier derivation). + /// @param insertionProof Relayer insertion proof advancing the active tree. + /// @param nullifiers Per-input active nullifiers η; bound across both proofs. + /// @param outputCommitments Output-note commitments (minted at currentEpoch). + /// @param root Commitment-tree root the spend proof was built against. + /// @param epochCreated Per-input note creation epoch. Bound to each input's + /// commitment by the spend proof, so a caller cannot misreport it. + /// @param postActiveRoot New active-tree root attested by the insertion proof. + /// @param encryptedNotes Encrypted output-note payloads for viewing-key holders. + function transfer( + bytes calldata spendProof, + bytes calldata insertionProof, + bytes32[2] calldata nullifiers, + bytes32[2] calldata outputCommitments, + bytes32 root, + uint64[2] calldata epochCreated, + bytes32 postActiveRoot, + bytes calldata encryptedNotes + ) external { + if (!isKnownRoot(root)) revert InvalidRoot(); + + // Spend proof: 11 public inputs in circuit declaration order (see + // circuits/transfer/src/main.nr). The contract pins current_epoch, + // chainVkHash, and the per-input expected accumulators. + bytes32[] memory spendInputs = new bytes32[](11); + spendInputs[0] = nullifiers[0]; + spendInputs[1] = nullifiers[1]; + spendInputs[2] = outputCommitments[0]; + spendInputs[3] = outputCommitments[1]; + spendInputs[4] = root; + spendInputs[5] = bytes32(uint256(currentEpoch)); + spendInputs[6] = chainVkHash; + spendInputs[7] = bytes32(uint256(epochCreated[0])); + spendInputs[8] = bytes32(uint256(epochCreated[1])); + spendInputs[9] = expectedChainAccumulator(epochCreated[0]); + spendInputs[10] = expectedChainAccumulator(epochCreated[1]); + if (!transferVerifier.verify(spendProof, spendInputs)) revert InvalidProof(); + + // Insertion proof (k=2). Cross-proof binding: the SAME `nullifiers` fed to + // the spend proof above are the insertion proof's η list, so one caller- + // supplied list pins both proofs to an identical list — a relayer whose + // insertion proof covers a different list fails the insertion verify, and a + // wallet whose spend proof covers a different list fails the spend verify. + // There is no second list to assert equal. + bytes32[] memory nullifierList = new bytes32[](2); + nullifierList[0] = nullifiers[0]; + nullifierList[1] = nullifiers[1]; + _verifyInsertionAndAdvance(insertionVerifier, insertionProof, nullifierList, postActiveRoot); + + // Append the two output commitments. + _insertCommitment(uint256(outputCommitments[0])); + _insertCommitment(uint256(outputCommitments[1])); + + emit Transfer(nullifiers[0], nullifiers[1], outputCommitments[0], outputCommitments[1], encryptedNotes); + } + + /// @notice Withdraw a single input note's full value to `recipient`, advancing + /// the active nullifier tree via the relayer's 1-insertion proof. + /// @dev Single-input spend path (SPEC "Withdraw (extended)"): the same two-proof + /// structure as `transfer` with k=1. State-pinned public inputs and the + /// cross-proof η binding work exactly as in `transfer` (which see). Funds + /// move out last, after all state changes (checks-effects-interactions). + /// @param spendProof Wallet withdraw spend proof. + /// @param insertionProof Relayer 1-insertion proof advancing the active tree. + /// @param nullifier The spent note's active nullifier η; bound across both proofs. + /// @param token ERC-20 token being withdrawn (bound to the note by the proof). + /// @param amount Amount leaving the pool (bound to the note by the proof). + /// @param recipient Address that receives the withdrawn funds. + /// @param root Commitment-tree root the spend proof was built against. + /// @param epochCreated The note's creation epoch (bound to its commitment by + /// the spend proof, so a caller cannot misreport it). + /// @param postActiveRoot New active-tree root attested by the insertion proof. + function withdraw( + bytes calldata spendProof, + bytes calldata insertionProof, + bytes32 nullifier, + address token, + uint256 amount, + address recipient, + bytes32 root, + uint64 epochCreated, + bytes32 postActiveRoot + ) external { + if (!supportedTokens[token]) revert UnsupportedToken(); + if (amount == 0) revert ZeroAmount(); + if (recipient == address(0)) revert ZeroAddress(); + if (!isKnownRoot(root)) revert InvalidRoot(); + + // Spend proof: 9 public inputs in circuit declaration order (see + // circuits/withdraw/src/main.nr). The contract pins current_epoch, + // chainVkHash, and the expected accumulator. + bytes32[] memory spendInputs = new bytes32[](9); + spendInputs[0] = nullifier; + spendInputs[1] = bytes32(uint256(uint160(token))); + spendInputs[2] = bytes32(amount); + spendInputs[3] = bytes32(uint256(uint160(recipient))); + spendInputs[4] = root; + spendInputs[5] = bytes32(uint256(currentEpoch)); + spendInputs[6] = chainVkHash; + spendInputs[7] = bytes32(uint256(epochCreated)); + spendInputs[8] = expectedChainAccumulator(epochCreated); + if (!withdrawVerifier.verify(spendProof, spendInputs)) revert InvalidProof(); + + // Insertion proof (k=1). Cross-proof binding: the SAME `nullifier` fed to + // the spend proof above is the insertion proof's η list (see `transfer`). + bytes32[] memory nullifierList = new bytes32[](1); + nullifierList[0] = nullifier; + _verifyInsertionAndAdvance(withdrawInsertionVerifier, insertionProof, nullifierList, postActiveRoot); + + // Interactions last: send the withdrawn funds out. + IERC20(token).safeTransfer(recipient, amount); + + emit Withdraw(nullifier, recipient, token, amount); + } + + /// @notice Roll over to the next epoch: freeze the current active-nullifier + /// root and reset the active tree. PoC: owner-only; production would + /// use a decentralized trigger. + /// @dev Emits `EpochRollover(frozenEpoch, frozenRoot)`. + function rolloverEpoch() external onlyOwner { + uint64 frozenEpoch = currentEpoch; + bytes32 frozenRoot = activeNullifierRoot; + + frozenNullifierRoots[frozenEpoch] = frozenRoot; + activeNullifierRoot = emptyImtRoot; + activeLeafCount = 1; + currentEpoch = frozenEpoch + 1; + + emit EpochRollover(frozenEpoch, frozenRoot); + } + + /// @notice Add a supported token. + function addSupportedToken(address token) external onlyOwner { + if (token == address(0)) revert ZeroAddress(); + if (supportedTokens[token]) revert TokenAlreadySupported(); + supportedTokens[token] = true; + emit TokenAdded(token); + } + + /// @notice Remove a supported token. + function removeSupportedToken(address token) external onlyOwner { + if (!supportedTokens[token]) revert TokenNotSupported(); + supportedTokens[token] = false; + emit TokenRemoved(token); + } + + /// @notice True if `root` is the current or a retained historical root. + function isKnownRoot(bytes32 root) public view returns (bool) { + if (root == commitmentRoot()) return true; + return validRoots[root]; + } + + /// @notice Update the deposit verifier. + function setDepositVerifier(address newVerifier) external onlyOwner { + if (newVerifier == address(0)) revert ZeroAddress(); + emit DepositVerifierUpdated(address(depositVerifier), newVerifier); + depositVerifier = IVerifier(newVerifier); + } + + /// @notice Transfer ownership of the contract. + function transferOwnership(address newOwner) external onlyOwner { + if (newOwner == address(0)) revert ZeroAddress(); + emit OwnershipTransferred(owner, newOwner); + owner = newOwner; + } + + /// @dev Verify the relayer's insertion proof and advance the active tree by + /// `nullifierList.length` leaves. Shared by `transfer` (k=2) and + /// `withdraw` (k=1). Insertion public inputs are + /// `[pre_active_root, post_active_root, pre_leaf_count, η_1..k]`; the + /// contract pins the pre-state (`activeNullifierRoot`/`activeLeafCount`), + /// so a proof built against a stale root reverts and the relayer rebuilds. + /// `nullifierList` is the shared η list that also feeds the spend proof, + /// which is what binds the two proofs together (see `transfer`). + function _verifyInsertionAndAdvance( + IVerifier insVerifier, + bytes calldata insertionProof, + bytes32[] memory nullifierList, + bytes32 postActiveRoot + ) internal { + uint256 k = nullifierList.length; + bytes32[] memory insertionInputs = new bytes32[](3 + k); + insertionInputs[0] = activeNullifierRoot; + insertionInputs[1] = postActiveRoot; + insertionInputs[2] = bytes32(uint256(activeLeafCount)); + for (uint256 i = 0; i < k; i++) { + insertionInputs[3 + i] = nullifierList[i]; + } + if (!insVerifier.verify(insertionProof, insertionInputs)) revert InvalidProof(); + + activeNullifierRoot = postActiveRoot; + activeLeafCount += uint64(k); + } + + /// @dev Insert a commitment, retaining the superseded root as historical. + function _insertCommitment(uint256 commitment) internal { + bytes32 currentRoot = commitmentRoot(); + if (currentRoot != bytes32(0)) { + bytes32 evictedRoot = historicalRoots[historicalRootIndex]; + if (evictedRoot != bytes32(0)) { + delete validRoots[evictedRoot]; + } + validRoots[currentRoot] = true; + historicalRoots[historicalRootIndex] = currentRoot; + historicalRootIndex = (historicalRootIndex + 1) % MAX_HISTORICAL_ROOTS; + } + _tree.insert(commitment); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/contracts/src/interfaces/IVerifier.sol b/pocs/private-payment/shielded-pool-extension/contracts/src/interfaces/IVerifier.sol new file mode 100644 index 0000000..111841a --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/contracts/src/interfaces/IVerifier.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IVerifier { + function verify(bytes calldata proof, bytes32[] calldata publicInputs) + external + view + returns (bool); +} diff --git a/pocs/private-payment/shielded-pool-extension/contracts/src/mocks/MockERC20.sol b/pocs/private-payment/shielded-pool-extension/contracts/src/mocks/MockERC20.sol new file mode 100644 index 0000000..cb05ba6 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/contracts/src/mocks/MockERC20.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20} from "@openzeppelin-contracts/token/ERC20/ERC20.sol"; + +/// @title MockERC20 +/// @notice Simple ERC20 token for testing with a public mint function. +contract MockERC20 is ERC20 { + uint8 private immutable _DECIMALS; + + constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) { + _DECIMALS = decimals_; + } + + function decimals() public view override returns (uint8) { + return _DECIMALS; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/contracts/src/mocks/MockVerifier.sol b/pocs/private-payment/shielded-pool-extension/contracts/src/mocks/MockVerifier.sol new file mode 100644 index 0000000..b8b2875 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/contracts/src/mocks/MockVerifier.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IVerifier} from "../interfaces/IVerifier.sol"; + +/// @title MockVerifier +/// @notice Configurable mock implementing the generic `IVerifier` for tests. +/// Returns `result` (default true) regardless of proof or inputs. +contract MockVerifier is IVerifier { + bool public result = true; + + function setResult(bool result_) external { + result = result_; + } + + function verify(bytes calldata, bytes32[] calldata) external view returns (bool) { + return result; + } +} diff --git a/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/DepositVerifier.sol b/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/DepositVerifier.sol new file mode 100644 index 0000000..8aec9eb --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/DepositVerifier.sol @@ -0,0 +1,2460 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec +pragma solidity >=0.8.21; + +uint256 constant N = 4096; +uint256 constant LOG_N = 12; +uint256 constant NUMBER_OF_PUBLIC_INPUTS = 12; +uint256 constant VK_HASH = 0x197556a16dadb7671cd101166d2ff2ebd972d598dd1692fb4d775fa0c4dc9915; +library HonkVerificationKey { + function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { + Honk.VerificationKey memory vk = Honk.VerificationKey({ + circuitSize: uint256(4096), + logCircuitSize: uint256(12), + publicInputsSize: uint256(12), + ql: Honk.G1Point({ + x: uint256(0x1334969407f4a606d3b22e37f187e458655c395a7802b50b7cb169ddb74e341a), + y: uint256(0x0dc62fa462ad33c366b80cfba27d6b815872c199da9d7ddbbad8881870f06999) + }), + qr: Honk.G1Point({ + x: uint256(0x06b6c6c5152a7548ddd51aad7adbcfe94cb3ebf7cb7c589c2a6d5ea5bd3aa3a9), + y: uint256(0x195f25ff26d9456d6d5271a218005106ba20ad927b2a136e6757f0d93141104d) + }), + qo: Honk.G1Point({ + x: uint256(0x0c94c9cd87163e872fc21500c5d94b13538fc1bdac9199a16e4651be10390f07), + y: uint256(0x0026db753281f1b37a0f0894d8b1b54cc62e2eb2fe036b87cc3a7ae3e12da323) + }), + q4: Honk.G1Point({ + x: uint256(0x028a5e827776076763a4d14cbb91dcb06299abd9c1b96481928c280581941b13), + y: uint256(0x08f876405852dec884a21e9ea3ebe79fa46b6c7e7aad09f55a477165ed66caba) + }), + qm: Honk.G1Point({ + x: uint256(0x0452fd39904ec81b48f55119255afaa36b0f2b65031d3080c7c072a9544ab4af), + y: uint256(0x14411af1c3271c092aaa2f78f61b1860a0c8b9d38e0a4001aecfaf887f56cc19) + }), + qc: Honk.G1Point({ + x: uint256(0x024200ac43e13aaa1d6bc60dcc7651235fcdfdfb9a159790657dfd05a3c15661), + y: uint256(0x096b610c628a97589ff4958a1534187be2e8afcca99dba6b07dedb7b8e382b77) + }), + qLookup: Honk.G1Point({ + x: uint256(0x13129f0714c3307644809495e0150426c814cd7c5e1ba2094969bb1d74f1c66b), + y: uint256(0x0e7d5873232b1bdd0ce181513b47d17d4549a4df958fe4825e7cb590563154ab) + }), + qArith: Honk.G1Point({ + x: uint256(0x101895e92be50fcaa1bd226b27036f8059351a0e8217078e3e623e6e974e47c6), + y: uint256(0x0e28d12071a0b71a952726240a53af64311efe08675b400c8c77efaf8a56ddd0) + }), + qDeltaRange: Honk.G1Point({ + x: uint256(0x2d738c59b466a6df8ecdc4e1640be618eba099c311fee9ee854b933ff0d6d6b7), + y: uint256(0x0237735817b3213a9a4362e0d4def3e0e52c731449931dd1f5441a05be7a1042) + }), + qElliptic: Honk.G1Point({ + x: uint256(0x15d676f9ec3bf0d6fb0104033f07a9034fa55278063b51006c34465ffdf3d156), + y: uint256(0x2425031560af64016b7b335131717e3efdb5b34619ae2aa0f98a7676ea5bbc97) + }), + qMemory: Honk.G1Point({ + x: uint256(0x178739cbeb1975dbe5ee996bb170bcc3cae20340b518a1527f54b8c61032c145), + y: uint256(0x0867cdc8340f67a37c953d7f18ca37c58760e8b4591bd62bd42134638693f84b) + }), + qNnf: Honk.G1Point({ + x: uint256(0x272bd27b92638f719db144e087ff47fe3b81858b931454e305f58d4456ac4794), + y: uint256(0x0ed22b1689756b980b8c0c8223d6bc12fd8023829a4724079eae22c0c04d3326) + }), + qPoseidon2External: Honk.G1Point({ + x: uint256(0x2af8e13a6542420e42dc338e51777836c4091b6eb61002be03555ce2dc790396), + y: uint256(0x14dcee749284175b1a65770c7f7d0ebfdcac55565d73755661a3baa596638142) + }), + qPoseidon2Internal: Honk.G1Point({ + x: uint256(0x137a05d84f2c263c02810ae3e8a1b7cc49bc6b0df848fd817e524170afc19bae), + y: uint256(0x192cc352b6902d2281a32e17eb47d5b73674579af1cf1161ac9e99f1296e868c) + }), + s1: Honk.G1Point({ + x: uint256(0x252f1b0a2a945c5e55a3382dcde45d06aedf74cc32fdec964cba0b2d37104619), + y: uint256(0x1b2a66c09306a87083984e76ed9411fd0e1996a109df0276cc51a67223877cba) + }), + s2: Honk.G1Point({ + x: uint256(0x1ee56549a56c1f29feff0a96685094febb960ee4306f9021f4ab550ca58f370a), + y: uint256(0x1a717fb59869f087b41d4e3624c10748bf982c3adaf8bae44f0dae950623a39b) + }), + s3: Honk.G1Point({ + x: uint256(0x18f1427ad7881741505dca79f0ac0196e923f248d25913f92a631d934e6f48fa), + y: uint256(0x1ee61cc7fd553ba57f52caf8fee11c275b27b4a9df5c159734c64ad4d4e1228a) + }), + s4: Honk.G1Point({ + x: uint256(0x159d31a7cbb98ea8c0b32347e5312eb96458d6dcab0b023d7d2af538502b6cf6), + y: uint256(0x0a89768d9823b175ad75cacb205f1ebbec5bfea3e48fa9ac73d7a8f3cc7ffb5b) + }), + t1: Honk.G1Point({ + x: uint256(0x099e3bd5a0a00ab7fe18040105b9b395b5d8b7b4a63b05df652b0d10ef146d26), + y: uint256(0x0015b8d2515d76e2ccec99dcd194592129af3a637f5a622a32440f860d1e2a7f) + }), + t2: Honk.G1Point({ + x: uint256(0x1b917517920bad3d8bc01c9595092a222b888108dc25d1aa450e0b4bc212c37e), + y: uint256(0x305e8992b148eedb22e6e992077a84482141c7ebe42000a1d58ccb74381f6d19) + }), + t3: Honk.G1Point({ + x: uint256(0x16465a5ccbb550cd2c63bd58116fe47c86847618681dc29d8a9363ab7c40e1c3), + y: uint256(0x2e24d420fbf9508ed31de692db477b439973ac12d7ca796d6fe98ca40e6ca6b7) + }), + t4: Honk.G1Point({ + x: uint256(0x043d063b130adfb37342af45d0155a28edd1a7e46c840d9c943fdf45521c64ce), + y: uint256(0x261522c4089330646aff96736194949330952ae74c573d1686d9cb4a00733854) + }), + id1: Honk.G1Point({ + x: uint256(0x1065fb6f70f149bfb7787992a8c13a0d48530a341d425781dd6f0105b7ed66e6), + y: uint256(0x2dfab2672cb7ab13c80c17ffd2fa97f1bc96f328c6dec1b94c8b39780ca58178) + }), + id2: Honk.G1Point({ + x: uint256(0x05ffa6cd4c43e71da27c942dc3846d80f13eaad0fe07e06ef9092cab3f18bbed), + y: uint256(0x302027d0930a91f441cdfa33ba9b5d57fc89838cda014d9f0003139f8640a7d1) + }), + id3: Honk.G1Point({ + x: uint256(0x2a3432a7ab6e77ea7b9c1fb09f4f306ae4385796ed64c8c9b06577390cf22ab1), + y: uint256(0x0c70188918ae051337cfd9b7a26f3247073276787cf119ee4996dc8788746b56) + }), + id4: Honk.G1Point({ + x: uint256(0x003cfca67bcf86fe53efa099c7a1fc7e870625ebff7f3f0803893eac661c9e9b), + y: uint256(0x05e480d9953688499ee120227781fcad6c820f56ceb34feeaa54233954a6ac67) + }), + lagrangeFirst: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000001), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000002) + }), + lagrangeLast: Honk.G1Point({ + x: uint256(0x2d86b23d5f0b76fa8d836a4259031a5178a50bde00d00b330eece611404433a0), + y: uint256(0x29f9a46f2b2b531e6a766224c56e83df18db09f2292ad4df9c075e64b83d4c8a) + }) + }); + return vk; + } +} + +pragma solidity ^0.8.27; + +interface IVerifier { + function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); +} + +/** + * @notice Library of error codes + * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier + */ +library Errors { + error ValueGeLimbMax(); + error ValueGeGroupOrder(); + error ValueGeFieldOrder(); + + error InvertOfZero(); + error NotPowerOfTwo(); + error ModExpFailed(); + + error ProofLengthWrong(); + error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); + error PublicInputsLengthWrong(); + error SumcheckFailed(); + error ShpleminiFailed(); + + error PointAtInfinity(); + + error ConsistencyCheckFailed(); + error GeminiChallengeInSubgroup(); +} + +type Fr is uint256; + +using {add as +} for Fr global; +using {sub as -} for Fr global; +using {mul as *} for Fr global; + +using {notEqual as !=} for Fr global; +using {equal as ==} for Fr global; + +uint256 constant SUBGROUP_SIZE = 256; +uint256 constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Prime field order +uint256 constant P = MODULUS; +Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); +Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); +Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); +Fr constant ONE = Fr.wrap(1); +Fr constant ZERO = Fr.wrap(0); +// Instantiation + +library FrLib { + bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; + + function invert(Fr value) internal view returns (Fr) { + uint256 v = Fr.unwrap(value); + require(v != 0, Errors.InvertOfZero()); + + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), v) + mstore(add(free, 0x80), sub(MODULUS, 2)) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function pow(Fr base, uint256 v) internal view returns (Fr) { + uint256 b = Fr.unwrap(base); + // Only works for power of 2 + require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), b) + mstore(add(free, 0x80), v) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function div(Fr numerator, Fr denominator) internal view returns (Fr) { + unchecked { + return numerator * invert(denominator); + } + } + + function sqr(Fr value) internal pure returns (Fr) { + unchecked { + return value * value; + } + } + + function unwrap(Fr value) internal pure returns (uint256) { + unchecked { + return Fr.unwrap(value); + } + } + + function neg(Fr value) internal pure returns (Fr) { + unchecked { + return Fr.wrap(MODULUS - Fr.unwrap(value)); + } + } + + function from(uint256 value) internal pure returns (Fr) { + unchecked { + require(value < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(value); + } + } + + function fromBytes32(bytes32 value) internal pure returns (Fr) { + unchecked { + uint256 v = uint256(value); + require(v < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(v); + } + } + + function toBytes32(Fr value) internal pure returns (bytes32) { + unchecked { + return bytes32(Fr.unwrap(value)); + } + } +} + +// Free functions +function add(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function mul(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function sub(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); + } +} + +function notEqual(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) != Fr.unwrap(b); + } +} + +function equal(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) == Fr.unwrap(b); + } +} + +uint256 constant CONST_PROOF_SIZE_LOG_N = 25; + +uint256 constant NUMBER_OF_SUBRELATIONS = 28; +uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; +uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; +uint256 constant NUMBER_OF_ENTITIES = 41; +// The number of entities added for ZK (gemini_masking_poly) +uint256 constant NUM_MASKING_POLYNOMIALS = 1; +uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_UNSHIFTED = 36; +uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_TO_BE_SHIFTED = 5; +uint256 constant PAIRING_POINTS_SIZE = 8; + +uint256 constant FIELD_ELEMENT_SIZE = 0x20; +uint256 constant GROUP_ELEMENT_SIZE = 0x40; + +// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) +uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; + +// ENUM FOR WIRES +enum WIRE { + Q_M, + Q_C, + Q_L, + Q_R, + Q_O, + Q_4, + Q_LOOKUP, + Q_ARITH, + Q_RANGE, + Q_ELLIPTIC, + Q_MEMORY, + Q_NNF, + Q_POSEIDON2_EXTERNAL, + Q_POSEIDON2_INTERNAL, + SIGMA_1, + SIGMA_2, + SIGMA_3, + SIGMA_4, + ID_1, + ID_2, + ID_3, + ID_4, + TABLE_1, + TABLE_2, + TABLE_3, + TABLE_4, + LAGRANGE_FIRST, + LAGRANGE_LAST, + W_L, + W_R, + W_O, + W_4, + Z_PERM, + LOOKUP_INVERSES, + LOOKUP_READ_COUNTS, + LOOKUP_READ_TAGS, + W_L_SHIFT, + W_R_SHIFT, + W_O_SHIFT, + W_4_SHIFT, + Z_PERM_SHIFT +} + +library Honk { + struct G1Point { + uint256 x; + uint256 y; + } + + struct VerificationKey { + // Misc Params + uint256 circuitSize; + uint256 logCircuitSize; + uint256 publicInputsSize; + // Selectors + G1Point qm; + G1Point qc; + G1Point ql; + G1Point qr; + G1Point qo; + G1Point q4; + G1Point qLookup; // Lookup + G1Point qArith; // Arithmetic widget + G1Point qDeltaRange; // Delta Range sort + G1Point qMemory; // Memory + G1Point qNnf; // Non-native Field + G1Point qElliptic; // Auxillary + G1Point qPoseidon2External; + G1Point qPoseidon2Internal; + // Copy constraints + G1Point s1; + G1Point s2; + G1Point s3; + G1Point s4; + // Copy identity + G1Point id1; + G1Point id2; + G1Point id3; + G1Point id4; + // Precomputed lookup table + G1Point t1; + G1Point t2; + G1Point t3; + G1Point t4; + // Fixed first and last + G1Point lagrangeFirst; + G1Point lagrangeLast; + } + + struct RelationParameters { + // challenges + Fr eta; + Fr beta; + Fr gamma; + // derived + Fr publicInputsDelta; + } + + struct Proof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // Free wires + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Lookup helpers - Permutations + G1Point zPerm; + // Lookup helpers - logup + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Sumcheck + Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + G1Point shplonkQ; + G1Point kzgQuotient; + } + + /// forge-lint: disable-next-item(pascal-case-struct) + struct ZKProof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) + G1Point geminiMaskingPoly; + // Commitments to wire polynomials + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Commitments to logup witness polynomials + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Commitment to grand permutation polynomial + G1Point zPerm; + G1Point[3] libraCommitments; + // Sumcheck + Fr libraSum; + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr libraEvaluation; + Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + Fr[4] libraPolyEvals; + G1Point shplonkQ; + G1Point kzgQuotient; + } +} + +// ZKTranscript library to generate fiat shamir challenges, the ZK transcript only differest +/// forge-lint: disable-next-item(pascal-case-struct) +struct ZKTranscript { + // Oink + Honk.RelationParameters relationParameters; + Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] + Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; + // Sumcheck + Fr libraChallenge; + Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; + // Shplemini + Fr rho; + Fr geminiR; + Fr shplonkNu; + Fr shplonkZ; + // Derived + Fr publicInputsDelta; +} + +library ZKTranscriptLib { + function generateTranscript( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + uint256 logN + ) external pure returns (ZKTranscript memory t) { + Fr previousChallenge; + (t.relationParameters, previousChallenge) = + generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); + + (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); + + (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); + (t.libraChallenge, previousChallenge) = generateLibraChallenge(previousChallenge, proof); + (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); + + (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); + + (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); + + (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); + + (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); + return t; + } + + function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { + uint256 challengeU256 = uint256(Fr.unwrap(challenge)); + // Split into two equal 127-bit chunks (254/2) + uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits + uint256 hi = challengeU256 >> 127; + first = FrLib.from(lo); + second = FrLib.from(hi); + } + + function generateRelationParametersChallenges( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + Fr previousChallenge + ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { + (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); + + (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); + } + + function generateEtaChallenge( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize + ) internal pure returns (Fr eta, Fr previousChallenge) { + // Size: 1 (vkHash) + publicInputsSize + 8 (geminiMask(2) + 3 wires(6)) + bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 8); + round0[0] = bytes32(vkHash); + + for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { + require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); + round0[1 + i] = publicInputs[i]; + } + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); + } + + // For ZK flavors: hash the gemini masking poly commitment (sent right after public inputs) + round0[1 + publicInputsSize] = bytes32(proof.geminiMaskingPoly.x); + round0[1 + publicInputsSize + 1] = bytes32(proof.geminiMaskingPoly.y); + + // Create the first challenge + // Note: w4 is added to the challenge later on + round0[1 + publicInputsSize + 2] = bytes32(proof.w1.x); + round0[1 + publicInputsSize + 3] = bytes32(proof.w1.y); + round0[1 + publicInputsSize + 4] = bytes32(proof.w2.x); + round0[1 + publicInputsSize + 5] = bytes32(proof.w2.y); + round0[1 + publicInputsSize + 6] = bytes32(proof.w3.x); + round0[1 + publicInputsSize + 7] = bytes32(proof.w3.y); + + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); + (eta,) = splitChallenge(previousChallenge); + } + + function generateBetaGammaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) + { + bytes32[7] memory round1; + round1[0] = FrLib.toBytes32(previousChallenge); + round1[1] = bytes32(proof.lookupReadCounts.x); + round1[2] = bytes32(proof.lookupReadCounts.y); + round1[3] = bytes32(proof.lookupReadTags.x); + round1[4] = bytes32(proof.lookupReadTags.y); + round1[5] = bytes32(proof.w4.x); + round1[6] = bytes32(proof.w4.y); + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); + (beta, gamma) = splitChallenge(nextPreviousChallenge); + } + + // Alpha challenges non-linearise the gate contributions + function generateAlphaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) + { + // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup + uint256[5] memory alpha0; + alpha0[0] = Fr.unwrap(previousChallenge); + alpha0[1] = proof.lookupInverses.x; + alpha0[2] = proof.lookupInverses.y; + alpha0[3] = proof.zPerm.x; + alpha0[4] = proof.zPerm.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); + Fr alpha; + (alpha,) = splitChallenge(nextPreviousChallenge); + + // Compute powers of alpha for batching subrelations + alphas[0] = alpha; + for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { + alphas[i] = alphas[i - 1] * alpha; + } + } + + function generateGateChallenges(Fr previousChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) + { + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); + (gateChallenges[0],) = splitChallenge(previousChallenge); + for (uint256 i = 1; i < logN; i++) { + gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; + } + nextPreviousChallenge = previousChallenge; + } + + function generateLibraChallenge(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr libraChallenge, Fr nextPreviousChallenge) + { + // 2 comm, 1 sum, 1 challenge + uint256[4] memory challengeData; + challengeData[0] = Fr.unwrap(previousChallenge); + challengeData[1] = proof.libraCommitments[0].x; + challengeData[2] = proof.libraCommitments[0].y; + challengeData[3] = Fr.unwrap(proof.libraSum); + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(challengeData))) % P); + (libraChallenge,) = splitChallenge(nextPreviousChallenge); + } + + function generateSumcheckChallenges(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) + { + for (uint256 i = 0; i < logN; i++) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; + univariateChal[0] = prevChallenge; + + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; + } + prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); + + (sumcheckChallenges[i],) = splitChallenge(prevChallenge); + } + nextPreviousChallenge = prevChallenge; + } + + // We add Libra claimed eval + 2 libra commitments (grand_sum, quotient) + function generateRhoChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr rho, Fr nextPreviousChallenge) + { + uint256[NUMBER_OF_ENTITIES_ZK + 6] memory rhoChallengeElements; + rhoChallengeElements[0] = Fr.unwrap(prevChallenge); + uint256 i; + for (i = 1; i <= NUMBER_OF_ENTITIES_ZK; i++) { + rhoChallengeElements[i] = Fr.unwrap(proof.sumcheckEvaluations[i - 1]); + } + rhoChallengeElements[i] = Fr.unwrap(proof.libraEvaluation); + i += 1; + rhoChallengeElements[i] = proof.libraCommitments[1].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[1].y; + i += 2; + rhoChallengeElements[i] = proof.libraCommitments[2].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[2].y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); + (rho,) = splitChallenge(nextPreviousChallenge); + } + + function generateGeminiRChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr geminiR, Fr nextPreviousChallenge) + { + uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); + gR[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 0; i < logN - 1; i++) { + gR[1 + i * 2] = proof.geminiFoldComms[i].x; + gR[2 + i * 2] = proof.geminiFoldComms[i].y; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); + + (geminiR,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkNuChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr shplonkNu, Fr nextPreviousChallenge) + { + uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1 + 4); + shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 1; i <= logN; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.geminiAEvaluations[i - 1]); + } + + uint256 libraIdx = 0; + for (uint256 i = logN + 1; i <= logN + 4; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.libraPolyEvals[libraIdx]); + libraIdx++; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); + (shplonkNu,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkZChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr shplonkZ, Fr nextPreviousChallenge) + { + uint256[3] memory shplonkZChallengeElements; + shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); + + shplonkZChallengeElements[1] = proof.shplonkQ.x; + shplonkZChallengeElements[2] = proof.shplonkQ.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); + (shplonkZ,) = splitChallenge(nextPreviousChallenge); + } + + function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.ZKProof memory p) { + uint256 boundary = 0x0; + + // Pairing point object + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); + // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 + require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); + p.pairingPointObject[i] = FrLib.from(limb); + boundary += FIELD_ELEMENT_SIZE; + } + + // Gemini masking polynomial commitment (sent first in ZK flavors, right after pairing points) + p.geminiMaskingPoly = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Commitments + p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Lookup / Permutation Helper Commitments + p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[0] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + p.libraSum = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + // Sumcheck univariates + for (uint256 i = 0; i < logN; i++) { + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + } + + // Sumcheck evaluations (includes gemini_masking_poly eval at index 0 for ZK flavors) + for (uint256 i = 0; i < NUMBER_OF_ENTITIES_ZK; i++) { + p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + p.libraEvaluation = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + + p.libraCommitments[1] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[2] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Gemini + // Read gemini fold univariates + for (uint256 i = 0; i < logN - 1; i++) { + p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + } + + // Read gemini a evaluations + for (uint256 i = 0; i < logN; i++) { + p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + for (uint256 i = 0; i < 4; i++) { + p.libraPolyEvals[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + // Shplonk + p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + // KZG + p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + } +} + +library RelationsLib { + struct EllipticParams { + // Points + Fr x_1; + Fr y_1; + Fr x_2; + Fr y_2; + Fr y_3; + Fr x_3; + // push accumulators into memory + Fr x_double_identity; + } + + // Parameters used within the Memory Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct MemParams { + Fr memory_record_check; + Fr partial_record_check; + Fr next_gate_access_type; + Fr record_delta; + Fr index_delta; + Fr adjacent_values_match_if_adjacent_indices_match; + Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; + Fr access_check; + Fr next_gate_access_type_is_boolean; + Fr ROM_consistency_check_identity; + Fr RAM_consistency_check_identity; + Fr timestamp_delta; + Fr RAM_timestamp_check_identity; + Fr memory_identity; + Fr index_is_monotonically_increasing; + } + + // Parameters used within the Non-Native Field Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct NnfParams { + Fr limb_subproduct; + Fr non_native_field_gate_1; + Fr non_native_field_gate_2; + Fr non_native_field_gate_3; + Fr limb_accumulator_1; + Fr limb_accumulator_2; + Fr nnf_identity; + } + + struct PoseidonExternalParams { + Fr s1; + Fr s2; + Fr s3; + Fr s4; + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr t0; + Fr t1; + Fr t2; + Fr t3; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr q_pos_by_scaling; + } + + struct PoseidonInternalParams { + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr u_sum; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr s1; + Fr q_pos_by_scaling; + } + + Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) + uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; + + // Constants for the Non-native Field relation + Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); + Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); + + function accumulateRelationEvaluations( + Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, + Fr powPartialEval + ) internal pure returns (Fr accumulator) { + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; + + // Accumulate all relations in Ultra Honk - each with varying number of subrelations + accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); + + // batch the subrelations with the precomputed alpha powers to obtain the full honk relation + accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); + } + + /** + * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids + * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code + * editors, and thus is noisy. + */ + function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { + return p[uint256(_wire)]; + } + + /** + * Ultra Arithmetic Relation + * + */ + function accumulateArithmeticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + // Relation 0 + Fr q_arith = wire(p, WIRE.Q_ARITH); + { + Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); + + Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; + accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) + + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); + accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); + accum = accum * q_arith; + accum = accum * domainSep; + evals[0] = accum; + } + + // Relation 1 + { + Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); + accum = accum * (q_arith - Fr.wrap(2)); + accum = accum * (q_arith - ONE); + accum = accum * q_arith; + accum = accum * domainSep; + evals[1] = accum; + } + } + + function accumulatePermutationRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr grand_product_numerator; + Fr grand_product_denominator; + + { + Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; + num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); + + grand_product_numerator = num; + } + { + Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; + den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); + + grand_product_denominator = den; + } + + // Contribution 2 + { + Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; + + acc = acc + - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) + * grand_product_denominator); + acc = acc * domainSep; + evals[2] = acc; + } + + // Contribution 3 + { + Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; + evals[3] = acc; + } + } + + function accumulateLogDerivativeLookupRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr table_term; + Fr lookup_term; + + // Calculate the write term (the table accumulation) + // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) + + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); + } + + // Calculate the read term + // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); + Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); + Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); + + lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) + + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); + } + + Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; + Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; + + Fr inverse_exists_xor = + wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) + - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); + + // Inverse calculated correctly relation + Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; + accumulatorNone = accumulatorNone * domainSep; + + // Inverse + Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; + + Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); + + Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; + + evals[4] = accumulatorNone; + evals[5] = accumulatorOne; + evals[6] = read_tag_boolean_relation * domainSep; + } + + function accumulateDeltaRangeRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr minus_one = ZERO - ONE; + Fr minus_two = ZERO - Fr.wrap(2); + Fr minus_three = ZERO - Fr.wrap(3); + + // Compute wire differences + Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); + Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); + Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); + Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); + + // Contribution 6 + { + Fr acc = delta_1; + acc = acc * (delta_1 + minus_one); + acc = acc * (delta_1 + minus_two); + acc = acc * (delta_1 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[7] = acc; + } + + // Contribution 7 + { + Fr acc = delta_2; + acc = acc * (delta_2 + minus_one); + acc = acc * (delta_2 + minus_two); + acc = acc * (delta_2 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[8] = acc; + } + + // Contribution 8 + { + Fr acc = delta_3; + acc = acc * (delta_3 + minus_one); + acc = acc * (delta_3 + minus_two); + acc = acc * (delta_3 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[9] = acc; + } + + // Contribution 9 + { + Fr acc = delta_4; + acc = acc * (delta_4 + minus_one); + acc = acc * (delta_4 + minus_two); + acc = acc * (delta_4 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[10] = acc; + } + } + + function accumulateEllipticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + EllipticParams memory ep; + ep.x_1 = wire(p, WIRE.W_R); + ep.y_1 = wire(p, WIRE.W_O); + + ep.x_2 = wire(p, WIRE.W_L_SHIFT); + ep.y_2 = wire(p, WIRE.W_4_SHIFT); + ep.y_3 = wire(p, WIRE.W_O_SHIFT); + ep.x_3 = wire(p, WIRE.W_R_SHIFT); + + Fr q_sign = wire(p, WIRE.Q_L); + Fr q_is_double = wire(p, WIRE.Q_M); + + // Contribution 10 point addition, x-coordinate check + // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 + Fr x_diff = (ep.x_2 - ep.x_1); + Fr y1_sqr = (ep.y_1 * ep.y_1); + { + // Move to top + Fr partialEval = domainSep; + + Fr y2_sqr = (ep.y_2 * ep.y_2); + Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; + Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); + x_add_identity = x_add_identity * x_diff * x_diff; + x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; + + evals[11] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 11 point addition, x-coordinate check + // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 + { + Fr y1_plus_y3 = ep.y_1 + ep.y_3; + Fr y_diff = ep.y_2 * q_sign - ep.y_1; + Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; + evals[12] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 10 point doubling, x-coordinate check + // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 + // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 + { + Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; + Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; + y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; + Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); + + // NOTE: pushed into memory (stack >:'( ) + ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; + + Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + evals[11] = evals[11] + acc; + } + + // Contribution 11 point doubling, y-coordinate check + // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 + { + Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; + Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); + evals[12] = evals[12] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + } + } + + function accumulateMemoryRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + MemParams memory ap; + + // Compute eta powers locally + Fr eta_two = rp.eta * rp.eta; + Fr eta_three = eta_two * rp.eta; + + /** + * MEMORY + * + * A RAM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) + * * v: `value` of memory cell being accessed + * * a: `access` type of record. read: 0 = read, 1 = write + * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three + * + * A ROM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three + * + * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + + * selectors, depending on whether the gate is a RAM read/write or a ROM read + * + * | gate type | i | v2/t | v | a | r | + * | --------- | -- | ----- | -- | -- | -- | + * | ROM | w1 | w2 | w3 | -- | w4 | + * | RAM | w1 | w2 | w3 | qc | w4 | + * + * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on + * `w2` to fix its value) + * + * + */ + + /** + * Memory Record Check + * Partial degree: 1 + * Total degree: 4 + * + * A ROM/ROM access gate can be evaluated with the identity: + * + * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 + * + * For ROM gates, qc = 0 + */ + ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); + ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); + ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 + ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); + + /** + * Contribution 13 & 14 + * ROM Consistency Check + * Partial degree: 1 + * Total degree: 4 + * + * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of + * records that are sorted. + * + * We apply the following checks for the sorted records: + * + * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 + * 2. index values for adjacent records are monotonically increasing + * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} + * + */ + ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); + ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); + + ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 + + ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 + + evals[14] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + evals[15] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + + ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 + + /** + * Contributions 15,16,17 + * RAM Consistency Check + * + * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` + * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. + * This is validated by requiring `access` to be boolean + * + * For two adjacent entries in the sorted list if _both_ + * A) index values match + * B) adjacent access value is 0 (i.e. next gate is a READ) + * then + * C) both values must match. + * The gate boolean check is + * (A && B) => C === !(A && B) || C === !A || !B || C + * + * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized + * with a WRITE operation. + */ + Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 + ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 + + // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta + // deg 1 or 4 + ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); + ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; + + Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); + ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = + (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 + + // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the + // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't + // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access + // type is correct, to cover this edge case + // deg 2 or 4 + ap.next_gate_access_type_is_boolean = + ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; + + // Putting it all together... + evals[16] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation + * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 + evals[17] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 + evals[18] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 6 + + ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 + + /** + * RAM Timestamp Consistency Check + * + * | w1 | w2 | w3 | w4 | + * | index | timestamp | timestamp_check | -- | + * + * Let delta_index = index_{i + 1} - index_{i} + * + * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i + * Else timestamp_check = 0 + */ + ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); + ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg 3 + + /** + * Complete Contribution 12 + * The complete RAM/ROM memory identity + * Partial degree: + */ + ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 + ap.memory_identity = + ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg 4 + ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 or 6 + ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 + + // (deg 3 or 9) + (deg 4) + (deg 3) + ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 + evals[13] = ap.memory_identity; + } + + function accumulateNnfRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + NnfParams memory ap; + + /** + * Contribution 12 + * Non native field arithmetic gate 2 + * deg 4 + * + * _ _ + * / _ _ _ 14 \ + * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | + * \_ _/ + * + * + */ + ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); + ap.non_native_field_gate_2 = + (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); + + ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; + ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); + ap.non_native_field_gate_1 = ap.limb_subproduct; + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); + + ap.non_native_field_gate_3 = ap.limb_subproduct; + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); + + Fr non_native_field_identity = + ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; + non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); + + // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm + // deg 2 + ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); + ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); + + // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm + // deg 2 + ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); + ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); + + Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; + limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 + + ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; + ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); + evals[19] = ap.nnf_identity; + } + + function accumulatePoseidonExternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonExternalParams memory ep; + + ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); + ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); + ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); + + ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; + ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; + ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; + ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; + // matrix mul v = M_E * u with 14 additions + ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 + ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 + ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 + // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 + ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 + // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 + ep.v4 = ep.t1 + ep.t1; + ep.v4 = ep.v4 + ep.v4 + ep.t3; + // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 + ep.v2 = ep.t0 + ep.t0; + ep.v2 = ep.v2 + ep.v2 + ep.t2; + // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 + ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 + ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 + + ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; + evals[20] = evals[20] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); + + evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); + + evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); + + evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + function accumulatePoseidonInternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonInternalParams memory ip; + + Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ + FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), + FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), + FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), + FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) + ]; + + // add round constants + ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + + // apply s-box round + ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; + ip.u2 = wire(p, WIRE.W_R); + ip.u3 = wire(p, WIRE.W_O); + ip.u4 = wire(p, WIRE.W_4); + + // matrix mul with v = M_I * u 4 muls and 7 additions + ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; + + ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; + + ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; + evals[24] = evals[24] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); + + ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; + evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); + + ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; + evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); + + ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; + evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + // Batch subrelation evaluations using precomputed powers of alpha + // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array + function scaleAndBatchSubrelations( + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges + ) internal pure returns (Fr accumulator) { + accumulator = evaluations[0]; + + for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { + accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; + } + } +} + +library CommitmentSchemeLib { + using FrLib for Fr; + + // Avoid stack too deep + struct ShpleminiIntermediates { + Fr unshiftedScalar; + Fr shiftedScalar; + Fr unshiftedScalarNeg; + Fr shiftedScalarNeg; + // Scalar to be multiplied by [1]₁ + Fr constantTermAccumulator; + // Accumulator for powers of rho + Fr batchingChallenge; + // Linear combination of multilinear (sumcheck) evaluations and powers of rho + Fr batchedEvaluation; + Fr[4] denominators; + Fr[4] batchingScalars; + // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr posInvertedDenominator; + // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr negInvertedDenominator; + // ν^{2i} * 1/(z - r^{2^i}) + Fr scalingFactorPos; + // ν^{2i+1} * 1/(z + r^{2^i}) + Fr scalingFactorNeg; + // Fold_i(r^{2^i}) reconstructed by Verifier + Fr[] foldPosEvaluations; + } + + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 + function computeFoldPosEvaluations( + Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, + Fr batchedEvalAccumulator, + Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, + Fr[] memory geminiEvalChallengePowers, + uint256 logSize + ) internal view returns (Fr[] memory) { + Fr[] memory foldPosEvaluations = new Fr[](logSize); + for (uint256 i = logSize; i > 0; --i) { + Fr challengePower = geminiEvalChallengePowers[i - 1]; + Fr u = sumcheckUChallenges[i - 1]; + + Fr batchedEvalRoundAcc = ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] + * (challengePower * (ONE - u) - u)); + // Divide by the denominator + batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); + + batchedEvalAccumulator = batchedEvalRoundAcc; + foldPosEvaluations[i - 1] = batchedEvalRoundAcc; + } + return foldPosEvaluations; + } + + function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { + Fr[] memory squares = new Fr[](logN); + squares[0] = r; + for (uint256 i = 1; i < logN; ++i) { + squares[i] = squares[i - 1].sqr(); + } + return squares; + } +} + +uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // EC group order. F_q + +// Fr utility + +function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { + scalar = FrLib.fromBytes32(bytes32(proofSection)); +} + +// EC Point utilities +function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { + uint256 x = uint256(bytes32(proofSection[0x00:0x20])); + uint256 y = uint256(bytes32(proofSection[0x20:0x40])); + require(x < Q && y < Q, Errors.ValueGeGroupOrder()); + + // Reject the point at infinity (0,0). EVM precompiles silently treat (0,0) + // as the identity element, which could zero out commitments. + // On-curve validation (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles + // per EIP-196, so we only need to catch this special case here. + require((x | y) != 0, Errors.PointAtInfinity()); + + point = Honk.G1Point({x: x, y: y}); +} + +function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { + // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. + if (point.y != 0) { + point.y = Q - point.y; + } + return point; +} + +/** + * Convert the pairing points to G1 points. + * + * The pairing points are serialised as an array of 2 limbs representing two points + * (P0 and P1, used for lhs and rhs of pairing operation). + * + * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. + * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] + * + * @param pairingPoints The pairing points to convert. + * @return lhs P0 point + * @return rhs P1 point + */ +function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) + pure + returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) +{ + // P0 (lhs): x = lo | (hi << 136) + uint256 lhsX = Fr.unwrap(pairingPoints[0]); + lhsX |= Fr.unwrap(pairingPoints[1]) << 136; + + uint256 lhsY = Fr.unwrap(pairingPoints[2]); + lhsY |= Fr.unwrap(pairingPoints[3]) << 136; + + // P1 (rhs): x = lo | (hi << 136) + uint256 rhsX = Fr.unwrap(pairingPoints[4]); + rhsX |= Fr.unwrap(pairingPoints[5]) << 136; + + uint256 rhsY = Fr.unwrap(pairingPoints[6]); + rhsY |= Fr.unwrap(pairingPoints[7]) << 136; + + // Reconstructed coordinates must be < Q to prevent malleability. + // Without this, two different limb encodings could map to the same curve point + // (via mulmod reduction in on-curve checks) but produce different transcript hashes. + require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); + + lhs.x = lhsX; + lhs.y = lhsY; + rhs.x = rhsX; + rhs.y = rhsY; +} + +/** + * Hash the pairing inputs from the present verification context with those extracted from the public inputs. + * + * @param proofPairingPoints Pairing points from the proof - (public inputs). + * @param accLhs Accumulator point for the left side - result of shplemini. + * @param accRhs Accumulator point for the right side - result of shplemini. + * @return recursionSeparator The recursion separator - generated from hashing the above. + */ +function generateRecursionSeparator( + Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, + Honk.G1Point memory accLhs, + Honk.G1Point memory accRhs +) pure returns (Fr recursionSeparator) { + // hash the proof aggregated X + // hash the proof aggregated Y + // hash the accum X + // hash the accum Y + + (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); + + uint256[8] memory recursionSeparatorElements; + + // Proof points + recursionSeparatorElements[0] = proofLhs.x; + recursionSeparatorElements[1] = proofLhs.y; + recursionSeparatorElements[2] = proofRhs.x; + recursionSeparatorElements[3] = proofRhs.y; + + // Accumulator points + recursionSeparatorElements[4] = accLhs.x; + recursionSeparatorElements[5] = accLhs.y; + recursionSeparatorElements[6] = accRhs.x; + recursionSeparatorElements[7] = accRhs.y; + + recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); +} + +/** + * G1 Mul with Separator + * Using the ecAdd and ecMul precompiles + * + * @param basePoint The point to multiply. + * @param other The other point to add. + * @param recursionSeperator The separator to use for the multiplication. + * @return `(recursionSeperator * basePoint) + other`. + */ +function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) + view + returns (Honk.G1Point memory) +{ + Honk.G1Point memory result; + + result = ecMul(recursionSeperator, basePoint); + result = ecAdd(result, other); + + return result; +} + +/** + * G1 Mul + * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. + * + * @param value The value to multiply the point by. + * @param point The point to multiply. + * @return result The result of the multiplication. + */ +function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write the point into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | point.x + // free + 0x20| point.y + mstore(free, mload(point)) + mstore(add(free, 0x20), mload(add(point, 0x20))) + // Write the scalar into memory (one 32 byte word) + // Memory layout: + // Address | value + // free + 0x40| value + mstore(add(free, 0x40), value) + + // Call the ecMul precompile, it takes in the following + // [point.x, point.y, scalar], and returns the result back into the free memory location. + let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) + if iszero(success) { + revert(0, 0) + } + // Copy the result of the multiplication back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x60)) + } + + return result; +} + +/** + * G1 Add + * Takes two G1 points and uses the ecAdd precompile to return the result. + * + * @param lhs The left hand side of the addition. + * @param rhs The right hand side of the addition. + * @return result The result of the addition. + */ +function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write lhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | lhs.x + // free + 0x20| lhs.y + mstore(free, mload(lhs)) + mstore(add(free, 0x20), mload(add(lhs, 0x20))) + + // Write rhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free + 0x40| rhs.x + // free + 0x60| rhs.y + mstore(add(free, 0x40), mload(rhs)) + mstore(add(free, 0x60), mload(add(rhs, 0x20))) + + // Call the ecAdd precompile, it takes in the following + // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. + let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) + if iszero(success) { revert(0, 0) } + + // Copy the result of the addition back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x80)) + } + + return result; +} + +function rejectPointAtInfinity(Honk.G1Point memory point) pure { + require((point.x | point.y) != 0, Errors.PointAtInfinity()); +} + +/** + * Check if pairing point limbs are all zero (default/infinity). + * Default pairing points indicate no recursive verification occurred. + */ +function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { + uint256 acc = 0; + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + acc |= Fr.unwrap(pairingPoints[i]); + } + return acc == 0; +} + +function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { + bytes memory input = abi.encodePacked( + rhs.x, + rhs.y, + // Fixed G2 point + uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), + uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), + uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), + uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), + lhs.x, + lhs.y, + // G2 point from VK + uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), + uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), + uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), + uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) + ); + + (bool success, bytes memory result) = address(0x08).staticcall(input); + decodedResult = success && abi.decode(result, (bool)); +} + +abstract contract BaseZKHonkVerifier is IVerifier { + using FrLib for Fr; + + struct PairingInputs { + Honk.G1Point P_0; + Honk.G1Point P_1; + } + + struct SmallSubgroupIpaIntermediates { + Fr[SUBGROUP_SIZE] challengePolyLagrange; + Fr challengePolyEval; + Fr lagrangeFirst; + Fr lagrangeLast; + Fr rootPower; + Fr[SUBGROUP_SIZE] denominators; // this has to disappear + Fr diff; + } + + // Constants for proof length calculation (matching UltraKeccakZKFlavor) + uint256 internal constant NUM_WITNESS_ENTITIES = 8 + NUM_MASKING_POLYNOMIALS; + uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points + uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements + uint256 internal constant NUM_LIBRA_EVALUATIONS = 4; // libra evaluations + + uint256 internal constant LIBRA_COMMITMENTS = 3; + uint256 internal constant LIBRA_EVALUATIONS = 4; + uint256 internal constant LIBRA_UNIVARIATES_LENGTH = 9; + + uint256 internal constant SHIFTED_COMMITMENTS_START = 30; + uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; + + uint256 internal immutable $N; + uint256 internal immutable $LOG_N; + uint256 internal immutable $VK_HASH; + uint256 internal immutable $NUM_PUBLIC_INPUTS; + uint256 internal immutable $MSMSize; + + constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { + $N = _N; + $LOG_N = _logN; + $VK_HASH = _vkHash; + $NUM_PUBLIC_INPUTS = _numPublicInputs; + $MSMSize = NUMBER_UNSHIFTED_ZK + _logN + LIBRA_COMMITMENTS + 2; + } + + function verify(bytes calldata proof, bytes32[] calldata publicInputs) + public + view + override + returns (bool verified) + { + // Calculate expected proof size based on $LOG_N + uint256 expectedProofSize = calculateProofSize($LOG_N); + + // Check the received proof is the expected size where each field element is 32 bytes + require( + proof.length == expectedProofSize, Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize) + ); + + Honk.VerificationKey memory vk = loadVerificationKey(); + Honk.ZKProof memory p = ZKTranscriptLib.loadProof(proof, $LOG_N); + + require(publicInputs.length == vk.publicInputsSize - PAIRING_POINTS_SIZE, Errors.PublicInputsLengthWrong()); + + // Generate the fiat shamir challenges for the whole protocol + ZKTranscript memory t = + ZKTranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); + + // Derive public input delta + t.relationParameters.publicInputsDelta = computePublicInputDelta( + publicInputs, + p.pairingPointObject, + t.relationParameters.beta, + t.relationParameters.gamma, /*pubInputsOffset=*/ + 1 + ); + + // Sumcheck + require(verifySumcheck(p, t), Errors.SumcheckFailed()); + require(verifyShplemini(p, vk, t), Errors.ShpleminiFailed()); + + verified = true; + } + + function computePublicInputDelta( + bytes32[] memory publicInputs, + Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, + Fr beta, + Fr gamma, + uint256 offset + ) internal view returns (Fr publicInputDelta) { + Fr numerator = Fr.wrap(1); + Fr denominator = Fr.wrap(1); + + Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); + Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); + + { + for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { + Fr pubInput = FrLib.fromBytes32(publicInputs[i]); + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + Fr pubInput = pairingPointObject[i]; + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + } + + // Fr delta = numerator / denominator; // TOOO: batch invert later? + publicInputDelta = FrLib.div(numerator, denominator); + } + + function verifySumcheck(Honk.ZKProof memory proof, ZKTranscript memory tp) internal view returns (bool verified) { + Fr roundTargetSum = tp.libraChallenge * proof.libraSum; // default 0 + Fr powPartialEvaluation = Fr.wrap(1); + + // We perform sumcheck reductions over log n rounds ( the multivariate degree ) + for (uint256 round; round < $LOG_N; ++round) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; + Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; + require(totalSum == roundTargetSum, Errors.SumcheckFailed()); + + Fr roundChallenge = tp.sumCheckUChallenges[round]; + + // Update the round target for the next rounf + roundTargetSum = computeNextTargetSum(roundUnivariate, roundChallenge); + powPartialEvaluation = + powPartialEvaluation * (Fr.wrap(1) + roundChallenge * (tp.gateChallenges[round] - Fr.wrap(1))); + } + + // Last round + // For ZK flavors: sumcheckEvaluations has 42 elements + // Index 0 is gemini_masking_poly, indices 1-41 are the regular entities used in relations + Fr[NUMBER_OF_ENTITIES] memory relationsEvaluations; + for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { + relationsEvaluations[i] = proof.sumcheckEvaluations[i + NUM_MASKING_POLYNOMIALS]; // Skip gemini_masking_poly at index 0 + } + Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( + relationsEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation + ); + + Fr evaluation = Fr.wrap(1); + for (uint256 i = 2; i < $LOG_N; i++) { + evaluation = evaluation * tp.sumCheckUChallenges[i]; + } + + grandHonkRelationSum = + grandHonkRelationSum * (Fr.wrap(1) - evaluation) + proof.libraEvaluation * tp.libraChallenge; + verified = (grandHonkRelationSum == roundTargetSum); + } + + // Return the new target sum for the next sumcheck round + function computeNextTargetSum(Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) + internal + view + returns (Fr targetSum) + { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000240), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80) + ]; + + // To compute the next target sum, we evaluate the given univariate at a point u (challenge). + + // Performing Barycentric evaluations + // Compute B(x) + Fr numeratorValue = Fr.wrap(1); + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); + } + + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + denominatorInverses[i] = FrLib.invert(BARYCENTRIC_LAGRANGE_DENOMINATORS[i] * (roundChallenge - Fr.wrap(i))); + } + + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + targetSum = targetSum + roundUnivariates[i] * denominatorInverses[i]; + } + + // Scale the sum by the value of B(x) + targetSum = targetSum * numeratorValue; + } + + function verifyShplemini(Honk.ZKProof memory proof, Honk.VerificationKey memory vk, ZKTranscript memory tp) + internal + view + returns (bool verified) + { + CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack + + // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size + Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); + // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings + Fr[] memory scalars = new Fr[]($MSMSize); + Honk.G1Point[] memory commitments = new Honk.G1Point[]($MSMSize); + + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); + + mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); + mem.shiftedScalar = + tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); + + scalars[0] = Fr.wrap(1); + commitments[0] = proof.shplonkQ; + + /* Batch multivariate opening claims, shifted and unshifted + * The vector of scalars is populated as follows: + * \f[ + * \left( + * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) + * \right) + * \f] + * + * The following vector is concatenated to the vector of commitments: + * \f[ + * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} + * \f] + * + * Simultaneously, the evaluation of the multilinear polynomial + * \f[ + * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} + * \f] + * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. + * + * This approach minimizes the number of iterations over the commitments to multilinear polynomials + * and eliminates the need to store the powers of \f$ \rho \f$. + */ + // For ZK flavors: evaluations array is [gemini_masking_poly, qm, qc, ql, qr, ...] + // Start batching challenge at 1, not rho, to match non-ZK pattern + mem.batchingChallenge = Fr.wrap(1); + mem.batchedEvaluation = Fr.wrap(0); + + mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); + mem.shiftedScalarNeg = mem.shiftedScalar.neg(); + + // Process all NUMBER_UNSHIFTED_ZK evaluations (includes gemini_masking_poly at index 0) + for (uint256 i = 1; i <= NUMBER_UNSHIFTED_ZK; ++i) { + scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; + mem.batchedEvaluation = mem.batchedEvaluation + + (proof.sumcheckEvaluations[i - NUM_MASKING_POLYNOMIALS] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + // g commitments are accumulated at r + // For each of the to be shifted commitments perform the shift in place by + // adding to the unshifted value. + // We do so, as the values are to be used in batchMul later, and as + // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. + // Applied to w1, w2, w3, w4 and zPerm + for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { + uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; + uint256 evaluationOff = i + NUMBER_UNSHIFTED_ZK; + + scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); + mem.batchedEvaluation = + mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + + commitments[1] = proof.geminiMaskingPoly; + + commitments[2] = vk.qm; + commitments[3] = vk.qc; + commitments[4] = vk.ql; + commitments[5] = vk.qr; + commitments[6] = vk.qo; + commitments[7] = vk.q4; + commitments[8] = vk.qLookup; + commitments[9] = vk.qArith; + commitments[10] = vk.qDeltaRange; + commitments[11] = vk.qElliptic; + commitments[12] = vk.qMemory; + commitments[13] = vk.qNnf; + commitments[14] = vk.qPoseidon2External; + commitments[15] = vk.qPoseidon2Internal; + commitments[16] = vk.s1; + commitments[17] = vk.s2; + commitments[18] = vk.s3; + commitments[19] = vk.s4; + commitments[20] = vk.id1; + commitments[21] = vk.id2; + commitments[22] = vk.id3; + commitments[23] = vk.id4; + commitments[24] = vk.t1; + commitments[25] = vk.t2; + commitments[26] = vk.t3; + commitments[27] = vk.t4; + commitments[28] = vk.lagrangeFirst; + commitments[29] = vk.lagrangeLast; + + // Accumulate proof points + commitments[30] = proof.w1; + commitments[31] = proof.w2; + commitments[32] = proof.w3; + commitments[33] = proof.w4; + commitments[34] = proof.zPerm; + commitments[35] = proof.lookupInverses; + commitments[36] = proof.lookupReadCounts; + commitments[37] = proof.lookupReadTags; + + /* Batch gemini claims from the prover + * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from + * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars + * + * 1. Moves the vector + * \f[ + * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) + * \f] + * to the 'commitments' vector. + * + * 2. Computes the scalars: + * \f[ + * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} + * \f] + * and places them into the 'scalars' vector. + * + * 3. Accumulates the summands of the constant term: + * \f[ + * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} + * \f] + * and adds them to the 'constant_term_accumulator'. + */ + + // Add contributions from A₀(r) and A₀(-r) to constant_term_accumulator: + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 + Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( + tp.sumCheckUChallenges, + mem.batchedEvaluation, + proof.geminiAEvaluations, + powers_of_evaluation_challenge, + $LOG_N + ); + + mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; + mem.constantTermAccumulator = + mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); + + mem.batchingChallenge = tp.shplonkNu.sqr(); + uint256 boundary = NUMBER_UNSHIFTED_ZK + 1; + + // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; + // Compute scalar multipliers for each fold commitment + for (uint256 i = 0; i < $LOG_N - 1; ++i) { + bool dummy_round = i >= ($LOG_N - 1); + + if (!dummy_round) { + // Update inverted denominators + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); + + // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] + mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; + mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; + scalars[boundary + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); + + // Accumulate the const term contribution given by + // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) + Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; + accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; + mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; + } + // Update the running power of v + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; + + commitments[boundary + i] = proof.geminiFoldComms[i]; + } + + boundary += $LOG_N - 1; + + // Finalize the batch opening claim + mem.denominators[0] = Fr.wrap(1).div(tp.shplonkZ - tp.geminiR); + mem.denominators[1] = Fr.wrap(1).div(tp.shplonkZ - SUBGROUP_GENERATOR * tp.geminiR); + mem.denominators[2] = mem.denominators[0]; + mem.denominators[3] = mem.denominators[0]; + + for (uint256 i = 0; i < LIBRA_EVALUATIONS; i++) { + Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; + mem.batchingScalars[i] = scalingFactor.neg(); + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; + mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i]; + } + scalars[boundary] = mem.batchingScalars[0]; + scalars[boundary + 1] = mem.batchingScalars[1] + mem.batchingScalars[2]; + scalars[boundary + 2] = mem.batchingScalars[3]; + + for (uint256 i = 0; i < LIBRA_COMMITMENTS; i++) { + commitments[boundary++] = proof.libraCommitments[i]; + } + + commitments[boundary] = Honk.G1Point({x: 1, y: 2}); + scalars[boundary++] = mem.constantTermAccumulator; + + require( + checkEvalsConsistency(proof.libraPolyEvals, tp.geminiR, tp.sumCheckUChallenges, proof.libraEvaluation), + Errors.ConsistencyCheckFailed() + ); + + Honk.G1Point memory quotient_commitment = proof.kzgQuotient; + + commitments[boundary] = quotient_commitment; + scalars[boundary] = tp.shplonkZ; // evaluation challenge + + PairingInputs memory pair; + pair.P_0 = batchMul(commitments, scalars); + pair.P_1 = negateInplace(quotient_commitment); + + // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) + if (!arePairingPointsDefault(proof.pairingPointObject)) { + Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, pair.P_0, pair.P_1); + (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = + convertPairingPointsToG1(proof.pairingPointObject); + + // Validate the points from the proof are on the curve + rejectPointAtInfinity(P_0_other); + rejectPointAtInfinity(P_1_other); + + // accumulate with aggregate points in proof + pair.P_0 = mulWithSeperator(pair.P_0, P_0_other, recursionSeparator); + pair.P_1 = mulWithSeperator(pair.P_1, P_1_other, recursionSeparator); + } + + return pairing(pair.P_0, pair.P_1); + } + + function checkEvalsConsistency( + Fr[LIBRA_EVALUATIONS] memory libraPolyEvals, + Fr geminiR, + Fr[CONST_PROOF_SIZE_LOG_N] memory uChallenges, + Fr libraEval + ) internal view returns (bool check) { + Fr one = Fr.wrap(1); + Fr vanishingPolyEval = geminiR.pow(SUBGROUP_SIZE) - one; + require(vanishingPolyEval != Fr.wrap(0), Errors.GeminiChallengeInSubgroup()); + + SmallSubgroupIpaIntermediates memory mem; + mem.challengePolyLagrange[0] = one; + for (uint256 round = 0; round < $LOG_N; round++) { + uint256 currIdx = 1 + LIBRA_UNIVARIATES_LENGTH * round; + mem.challengePolyLagrange[currIdx] = one; + for (uint256 idx = currIdx + 1; idx < currIdx + LIBRA_UNIVARIATES_LENGTH; idx++) { + mem.challengePolyLagrange[idx] = mem.challengePolyLagrange[idx - 1] * uChallenges[round]; + } + } + + mem.rootPower = one; + mem.challengePolyEval = Fr.wrap(0); + for (uint256 idx = 0; idx < SUBGROUP_SIZE; idx++) { + mem.denominators[idx] = mem.rootPower * geminiR - one; + mem.denominators[idx] = mem.denominators[idx].invert(); + mem.challengePolyEval = mem.challengePolyEval + mem.challengePolyLagrange[idx] * mem.denominators[idx]; + mem.rootPower = mem.rootPower * SUBGROUP_GENERATOR_INVERSE; + } + + Fr numerator = vanishingPolyEval * Fr.wrap(SUBGROUP_SIZE).invert(); + mem.challengePolyEval = mem.challengePolyEval * numerator; + mem.lagrangeFirst = mem.denominators[0] * numerator; + mem.lagrangeLast = mem.denominators[SUBGROUP_SIZE - 1] * numerator; + + mem.diff = mem.lagrangeFirst * libraPolyEvals[2]; + + mem.diff = mem.diff + (geminiR - SUBGROUP_GENERATOR_INVERSE) + * (libraPolyEvals[1] - libraPolyEvals[2] - libraPolyEvals[0] * mem.challengePolyEval); + mem.diff = mem.diff + mem.lagrangeLast * (libraPolyEvals[2] - libraEval) - vanishingPolyEval * libraPolyEvals[3]; + + check = mem.diff == Fr.wrap(0); + } + + // This implementation is the same as above with different constants + function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) + internal + view + returns (Honk.G1Point memory result) + { + uint256 limit = $MSMSize; + + // Validate all points are on the curve + for (uint256 i = 0; i < limit; ++i) { + rejectPointAtInfinity(base[i]); + } + + bool success = true; + assembly { + let free := mload(0x40) + + let count := 0x01 + for {} lt(count, add(limit, 1)) { count := add(count, 1) } { + // Get loop offsets + let base_base := add(base, mul(count, 0x20)) + let scalar_base := add(scalars, mul(count, 0x20)) + + mstore(add(free, 0x40), mload(mload(base_base))) + mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) + // Add scalar + mstore(add(free, 0x80), mload(scalar_base)) + + success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) + // accumulator = accumulator + accumulator_2 + success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) + } + + // Return the result + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + } + + require(success, Errors.ShpleminiFailed()); + } + + // Calculate proof size based on log_n (matching UltraKeccakZKFlavor formula) + function calculateProofSize(uint256 logN) internal pure returns (uint256) { + // Witness and Libra commitments + uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments + proofLength += NUM_ELEMENTS_COMM * 3; // Libra concat, grand sum, quotient comms + Gemini masking + + // Sumcheck + proofLength += logN * ZK_BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates + proofLength += NUMBER_OF_ENTITIES_ZK * NUM_ELEMENTS_FR; // sumcheck evaluations + + // Libra and Gemini + proofLength += NUM_ELEMENTS_FR * 2; // Libra sum, claimed eval + proofLength += logN * NUM_ELEMENTS_FR; // Gemini a evaluations + proofLength += NUM_LIBRA_EVALUATIONS * NUM_ELEMENTS_FR; // libra evaluations + + // PCS commitments + proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments + proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments + + // Pairing points + proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs + + return proofLength * 32; + } + + function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); +} + +contract HonkVerifier is BaseZKHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { + function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { + return HonkVerificationKey.loadVerificationKey(); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/InsertionVerifier.sol b/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/InsertionVerifier.sol new file mode 100644 index 0000000..4edeaa5 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/InsertionVerifier.sol @@ -0,0 +1,2460 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec +pragma solidity >=0.8.21; + +uint256 constant N = 524288; +uint256 constant LOG_N = 19; +uint256 constant NUMBER_OF_PUBLIC_INPUTS = 13; +uint256 constant VK_HASH = 0x2164aa1a8cdedffed8e0d5898d900c6d8dc75490bfe673e7f8c017842aa6eedf; +library HonkVerificationKey { + function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { + Honk.VerificationKey memory vk = Honk.VerificationKey({ + circuitSize: uint256(524288), + logCircuitSize: uint256(19), + publicInputsSize: uint256(13), + ql: Honk.G1Point({ + x: uint256(0x1bc364c4991ceabff107118e5e80574bc87b7bd9d522f71decb94c672c899d78), + y: uint256(0x1f719f0d3f817ca182087c8c332a1737c99f1bd4234fe47329dd5b7689aed04c) + }), + qr: Honk.G1Point({ + x: uint256(0x1c02c1bf580781c3faf1f773186d6ff179e307c32f9f7dbc72fca49d2cdacc8a), + y: uint256(0x095c79ce5bc6031c56c4100c497c4c109dc3424c12bf02607077b04d2beb600d) + }), + qo: Honk.G1Point({ + x: uint256(0x2ff780fecdb36fef447285c796497060c0fde11529ca30707ab854b2fd805f4d), + y: uint256(0x2493bf070f5e589bce1d6e13a872b1ca5efe6325cb0445e4b844c58e428f3b02) + }), + q4: Honk.G1Point({ + x: uint256(0x03d2c490d8c2d51a2bb32b89ac729a7e3a5208954ca65d57f84a983a370b3d0e), + y: uint256(0x0778ee6f7a228322723c01e908da5ac5a313456667ddfe095cdfbc250563a87b) + }), + qm: Honk.G1Point({ + x: uint256(0x02d4d8c25a41d0e83cd12ba4c374032803a2fc23a4c43e8c69a2600d7f121bf2), + y: uint256(0x0adaf4caffb8b593d4fed864a6321f80b586d679c32ed52bc5d1a7d214a6b4c8) + }), + qc: Honk.G1Point({ + x: uint256(0x290d34877b59b87081f68c2cde46306059375666cc1cc9be551ddcc90c27b189), + y: uint256(0x06932ed32dfd9947b9bf8bc5d4b3360f666c3f1e62992401fae9028fd847f499) + }), + qLookup: Honk.G1Point({ + x: uint256(0x2ccf97777978aa36adcdd05588e03d4de649085cda7fe019d46d0ec179c56274), + y: uint256(0x0973ebba2ca18abf3ad084c6fc0c7ef2dccadbc4f0cb96e4c98e950a23e2b847) + }), + qArith: Honk.G1Point({ + x: uint256(0x2e6d193e296c736f390cf54da3e29577eebb213877e614fbad709cc8ae9c2e96), + y: uint256(0x1edf5091d437ea7438dda9369bbf916fb4b6ea4216e842cc145176a5a56cd49d) + }), + qDeltaRange: Honk.G1Point({ + x: uint256(0x0591bb5c80b2a4304dc48409e524215ac9151170b6ebcea1c56da61b19dfe21b), + y: uint256(0x2f216d6b07c77b76b9d7771f757a087de2016e97740bc2ed1e5e4d8c5a49e945) + }), + qElliptic: Honk.G1Point({ + x: uint256(0x0236b4641b93bc2289071cad1c41a0b30a2345e75a6441e422dba86c301a5416), + y: uint256(0x20538a488c70d1f8a7aa69830cea9bea33c16b4178e3a90f65f4e4d50daef8d6) + }), + qMemory: Honk.G1Point({ + x: uint256(0x0ac2f1b3505a15a58bc3528ed8f27f32cccab6dd531b390bfd5fde985d36e965), + y: uint256(0x1b7fc4d0b085713e82e8b1f27ab2c360fd69172bdacad69757929087a59b0eaa) + }), + qNnf: Honk.G1Point({ + x: uint256(0x174f6a8c9694b4c3de28c844adec9f05c07d2e17d7d08131e131d78429a69031), + y: uint256(0x2d9c73e17577d47ffb05b3d094a90af4798765f53e38de2be544e78cc56c5f89) + }), + qPoseidon2External: Honk.G1Point({ + x: uint256(0x2e3d84c7143d3548eca7eb275c1d4805fdd645f6e1853984a7817a2603f56f09), + y: uint256(0x22d3e49197b063dc5ff6d1db2ae63b76db7b362d3c275ec0384804975d3e87ef) + }), + qPoseidon2Internal: Honk.G1Point({ + x: uint256(0x0eeb590ffc9b1d23e91b32cfa99c0c34dfa73188a23fc5d170b91f5080fe28aa), + y: uint256(0x27393b3146fd8db5530a434a16af73d84773ba3e8c76c96ea4fe6f225fbc550c) + }), + s1: Honk.G1Point({ + x: uint256(0x0da4cb806349aab9a023ca44d4fbb4b78a3ed9af1dc9fd1bf9de0ddf695be38c), + y: uint256(0x126484fccd7989423093a47b9f1f00a8a712bb839916d066ad58af309040c057) + }), + s2: Honk.G1Point({ + x: uint256(0x0da8f1de4d02d124d4c2231baf3c83b284673b7d38755f9b0717f9d8fa145ea5), + y: uint256(0x252baa9f766de148629b8f2265fb4a3df9622244a2c1920ea96f0d398263b79e) + }), + s3: Honk.G1Point({ + x: uint256(0x1a85758ed36566e8d26926865679b9d39218dc9a7752ed039f1c883bfe2ba828), + y: uint256(0x108f4577abc654e0ba2218f7d0465eb945eb790f2439a414e40effbb2c333763) + }), + s4: Honk.G1Point({ + x: uint256(0x2f6d68acb0fc89d2abc9676672bc212ba73249817667838c4bfa446559295011), + y: uint256(0x1419063b792cb11799a7d4191397f089fc51be3c8da15dc0323b012732c7909d) + }), + t1: Honk.G1Point({ + x: uint256(0x099e3bd5a0a00ab7fe18040105b9b395b5d8b7b4a63b05df652b0d10ef146d26), + y: uint256(0x0015b8d2515d76e2ccec99dcd194592129af3a637f5a622a32440f860d1e2a7f) + }), + t2: Honk.G1Point({ + x: uint256(0x1b917517920bad3d8bc01c9595092a222b888108dc25d1aa450e0b4bc212c37e), + y: uint256(0x305e8992b148eedb22e6e992077a84482141c7ebe42000a1d58ccb74381f6d19) + }), + t3: Honk.G1Point({ + x: uint256(0x16465a5ccbb550cd2c63bd58116fe47c86847618681dc29d8a9363ab7c40e1c3), + y: uint256(0x2e24d420fbf9508ed31de692db477b439973ac12d7ca796d6fe98ca40e6ca6b7) + }), + t4: Honk.G1Point({ + x: uint256(0x043d063b130adfb37342af45d0155a28edd1a7e46c840d9c943fdf45521c64ce), + y: uint256(0x261522c4089330646aff96736194949330952ae74c573d1686d9cb4a00733854) + }), + id1: Honk.G1Point({ + x: uint256(0x20c2f31f1449ff07d895054a8566bca2fd3fba5180e46d665f77953379d8c198), + y: uint256(0x136851cf4977f6c9bbb936b0af3bb008e020ebe16bd5dcb00abc41842c765f91) + }), + id2: Honk.G1Point({ + x: uint256(0x02d1cb40e9c26517101b9c00448dde44038f4b2efb7c78a0fe4057b8a3f46264), + y: uint256(0x305b20bee6f9b858e64d5e93afca3d13587993ad24ab1915009d25211ad2944a) + }), + id3: Honk.G1Point({ + x: uint256(0x26532b2daa10ac0c832fb979e3374f98cfdb2240a7ba48a1137453f51e622791), + y: uint256(0x2c0e44176eb78633cde29068be5ac2f0f69e020c1ae4da4ea431942a4f630ad5) + }), + id4: Honk.G1Point({ + x: uint256(0x2639b5fd11479764222aadf7e62a58a65aa37d4643e97f824435df446be8b999), + y: uint256(0x1e75235d6488d958da20827948cb5efdeac68335fc4fc77bcb8d8f86868cda84) + }), + lagrangeFirst: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000001), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000002) + }), + lagrangeLast: Honk.G1Point({ + x: uint256(0x23a1c4ce054fb20064f67716a013dc85f29fe27da6c398194a420cd22265e9ba), + y: uint256(0x0e7d8a8b84966ad3fb49e5eec16a957f0f4cd4cd63932ba161d28a2097ba64af) + }) + }); + return vk; + } +} + +pragma solidity ^0.8.27; + +interface IVerifier { + function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); +} + +/** + * @notice Library of error codes + * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier + */ +library Errors { + error ValueGeLimbMax(); + error ValueGeGroupOrder(); + error ValueGeFieldOrder(); + + error InvertOfZero(); + error NotPowerOfTwo(); + error ModExpFailed(); + + error ProofLengthWrong(); + error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); + error PublicInputsLengthWrong(); + error SumcheckFailed(); + error ShpleminiFailed(); + + error PointAtInfinity(); + + error ConsistencyCheckFailed(); + error GeminiChallengeInSubgroup(); +} + +type Fr is uint256; + +using {add as +} for Fr global; +using {sub as -} for Fr global; +using {mul as *} for Fr global; + +using {notEqual as !=} for Fr global; +using {equal as ==} for Fr global; + +uint256 constant SUBGROUP_SIZE = 256; +uint256 constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Prime field order +uint256 constant P = MODULUS; +Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); +Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); +Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); +Fr constant ONE = Fr.wrap(1); +Fr constant ZERO = Fr.wrap(0); +// Instantiation + +library FrLib { + bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; + + function invert(Fr value) internal view returns (Fr) { + uint256 v = Fr.unwrap(value); + require(v != 0, Errors.InvertOfZero()); + + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), v) + mstore(add(free, 0x80), sub(MODULUS, 2)) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function pow(Fr base, uint256 v) internal view returns (Fr) { + uint256 b = Fr.unwrap(base); + // Only works for power of 2 + require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), b) + mstore(add(free, 0x80), v) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function div(Fr numerator, Fr denominator) internal view returns (Fr) { + unchecked { + return numerator * invert(denominator); + } + } + + function sqr(Fr value) internal pure returns (Fr) { + unchecked { + return value * value; + } + } + + function unwrap(Fr value) internal pure returns (uint256) { + unchecked { + return Fr.unwrap(value); + } + } + + function neg(Fr value) internal pure returns (Fr) { + unchecked { + return Fr.wrap(MODULUS - Fr.unwrap(value)); + } + } + + function from(uint256 value) internal pure returns (Fr) { + unchecked { + require(value < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(value); + } + } + + function fromBytes32(bytes32 value) internal pure returns (Fr) { + unchecked { + uint256 v = uint256(value); + require(v < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(v); + } + } + + function toBytes32(Fr value) internal pure returns (bytes32) { + unchecked { + return bytes32(Fr.unwrap(value)); + } + } +} + +// Free functions +function add(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function mul(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function sub(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); + } +} + +function notEqual(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) != Fr.unwrap(b); + } +} + +function equal(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) == Fr.unwrap(b); + } +} + +uint256 constant CONST_PROOF_SIZE_LOG_N = 25; + +uint256 constant NUMBER_OF_SUBRELATIONS = 28; +uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; +uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; +uint256 constant NUMBER_OF_ENTITIES = 41; +// The number of entities added for ZK (gemini_masking_poly) +uint256 constant NUM_MASKING_POLYNOMIALS = 1; +uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_UNSHIFTED = 36; +uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_TO_BE_SHIFTED = 5; +uint256 constant PAIRING_POINTS_SIZE = 8; + +uint256 constant FIELD_ELEMENT_SIZE = 0x20; +uint256 constant GROUP_ELEMENT_SIZE = 0x40; + +// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) +uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; + +// ENUM FOR WIRES +enum WIRE { + Q_M, + Q_C, + Q_L, + Q_R, + Q_O, + Q_4, + Q_LOOKUP, + Q_ARITH, + Q_RANGE, + Q_ELLIPTIC, + Q_MEMORY, + Q_NNF, + Q_POSEIDON2_EXTERNAL, + Q_POSEIDON2_INTERNAL, + SIGMA_1, + SIGMA_2, + SIGMA_3, + SIGMA_4, + ID_1, + ID_2, + ID_3, + ID_4, + TABLE_1, + TABLE_2, + TABLE_3, + TABLE_4, + LAGRANGE_FIRST, + LAGRANGE_LAST, + W_L, + W_R, + W_O, + W_4, + Z_PERM, + LOOKUP_INVERSES, + LOOKUP_READ_COUNTS, + LOOKUP_READ_TAGS, + W_L_SHIFT, + W_R_SHIFT, + W_O_SHIFT, + W_4_SHIFT, + Z_PERM_SHIFT +} + +library Honk { + struct G1Point { + uint256 x; + uint256 y; + } + + struct VerificationKey { + // Misc Params + uint256 circuitSize; + uint256 logCircuitSize; + uint256 publicInputsSize; + // Selectors + G1Point qm; + G1Point qc; + G1Point ql; + G1Point qr; + G1Point qo; + G1Point q4; + G1Point qLookup; // Lookup + G1Point qArith; // Arithmetic widget + G1Point qDeltaRange; // Delta Range sort + G1Point qMemory; // Memory + G1Point qNnf; // Non-native Field + G1Point qElliptic; // Auxillary + G1Point qPoseidon2External; + G1Point qPoseidon2Internal; + // Copy constraints + G1Point s1; + G1Point s2; + G1Point s3; + G1Point s4; + // Copy identity + G1Point id1; + G1Point id2; + G1Point id3; + G1Point id4; + // Precomputed lookup table + G1Point t1; + G1Point t2; + G1Point t3; + G1Point t4; + // Fixed first and last + G1Point lagrangeFirst; + G1Point lagrangeLast; + } + + struct RelationParameters { + // challenges + Fr eta; + Fr beta; + Fr gamma; + // derived + Fr publicInputsDelta; + } + + struct Proof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // Free wires + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Lookup helpers - Permutations + G1Point zPerm; + // Lookup helpers - logup + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Sumcheck + Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + G1Point shplonkQ; + G1Point kzgQuotient; + } + + /// forge-lint: disable-next-item(pascal-case-struct) + struct ZKProof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) + G1Point geminiMaskingPoly; + // Commitments to wire polynomials + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Commitments to logup witness polynomials + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Commitment to grand permutation polynomial + G1Point zPerm; + G1Point[3] libraCommitments; + // Sumcheck + Fr libraSum; + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr libraEvaluation; + Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + Fr[4] libraPolyEvals; + G1Point shplonkQ; + G1Point kzgQuotient; + } +} + +// ZKTranscript library to generate fiat shamir challenges, the ZK transcript only differest +/// forge-lint: disable-next-item(pascal-case-struct) +struct ZKTranscript { + // Oink + Honk.RelationParameters relationParameters; + Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] + Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; + // Sumcheck + Fr libraChallenge; + Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; + // Shplemini + Fr rho; + Fr geminiR; + Fr shplonkNu; + Fr shplonkZ; + // Derived + Fr publicInputsDelta; +} + +library ZKTranscriptLib { + function generateTranscript( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + uint256 logN + ) external pure returns (ZKTranscript memory t) { + Fr previousChallenge; + (t.relationParameters, previousChallenge) = + generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); + + (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); + + (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); + (t.libraChallenge, previousChallenge) = generateLibraChallenge(previousChallenge, proof); + (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); + + (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); + + (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); + + (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); + + (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); + return t; + } + + function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { + uint256 challengeU256 = uint256(Fr.unwrap(challenge)); + // Split into two equal 127-bit chunks (254/2) + uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits + uint256 hi = challengeU256 >> 127; + first = FrLib.from(lo); + second = FrLib.from(hi); + } + + function generateRelationParametersChallenges( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + Fr previousChallenge + ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { + (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); + + (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); + } + + function generateEtaChallenge( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize + ) internal pure returns (Fr eta, Fr previousChallenge) { + // Size: 1 (vkHash) + publicInputsSize + 8 (geminiMask(2) + 3 wires(6)) + bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 8); + round0[0] = bytes32(vkHash); + + for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { + require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); + round0[1 + i] = publicInputs[i]; + } + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); + } + + // For ZK flavors: hash the gemini masking poly commitment (sent right after public inputs) + round0[1 + publicInputsSize] = bytes32(proof.geminiMaskingPoly.x); + round0[1 + publicInputsSize + 1] = bytes32(proof.geminiMaskingPoly.y); + + // Create the first challenge + // Note: w4 is added to the challenge later on + round0[1 + publicInputsSize + 2] = bytes32(proof.w1.x); + round0[1 + publicInputsSize + 3] = bytes32(proof.w1.y); + round0[1 + publicInputsSize + 4] = bytes32(proof.w2.x); + round0[1 + publicInputsSize + 5] = bytes32(proof.w2.y); + round0[1 + publicInputsSize + 6] = bytes32(proof.w3.x); + round0[1 + publicInputsSize + 7] = bytes32(proof.w3.y); + + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); + (eta,) = splitChallenge(previousChallenge); + } + + function generateBetaGammaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) + { + bytes32[7] memory round1; + round1[0] = FrLib.toBytes32(previousChallenge); + round1[1] = bytes32(proof.lookupReadCounts.x); + round1[2] = bytes32(proof.lookupReadCounts.y); + round1[3] = bytes32(proof.lookupReadTags.x); + round1[4] = bytes32(proof.lookupReadTags.y); + round1[5] = bytes32(proof.w4.x); + round1[6] = bytes32(proof.w4.y); + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); + (beta, gamma) = splitChallenge(nextPreviousChallenge); + } + + // Alpha challenges non-linearise the gate contributions + function generateAlphaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) + { + // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup + uint256[5] memory alpha0; + alpha0[0] = Fr.unwrap(previousChallenge); + alpha0[1] = proof.lookupInverses.x; + alpha0[2] = proof.lookupInverses.y; + alpha0[3] = proof.zPerm.x; + alpha0[4] = proof.zPerm.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); + Fr alpha; + (alpha,) = splitChallenge(nextPreviousChallenge); + + // Compute powers of alpha for batching subrelations + alphas[0] = alpha; + for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { + alphas[i] = alphas[i - 1] * alpha; + } + } + + function generateGateChallenges(Fr previousChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) + { + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); + (gateChallenges[0],) = splitChallenge(previousChallenge); + for (uint256 i = 1; i < logN; i++) { + gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; + } + nextPreviousChallenge = previousChallenge; + } + + function generateLibraChallenge(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr libraChallenge, Fr nextPreviousChallenge) + { + // 2 comm, 1 sum, 1 challenge + uint256[4] memory challengeData; + challengeData[0] = Fr.unwrap(previousChallenge); + challengeData[1] = proof.libraCommitments[0].x; + challengeData[2] = proof.libraCommitments[0].y; + challengeData[3] = Fr.unwrap(proof.libraSum); + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(challengeData))) % P); + (libraChallenge,) = splitChallenge(nextPreviousChallenge); + } + + function generateSumcheckChallenges(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) + { + for (uint256 i = 0; i < logN; i++) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; + univariateChal[0] = prevChallenge; + + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; + } + prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); + + (sumcheckChallenges[i],) = splitChallenge(prevChallenge); + } + nextPreviousChallenge = prevChallenge; + } + + // We add Libra claimed eval + 2 libra commitments (grand_sum, quotient) + function generateRhoChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr rho, Fr nextPreviousChallenge) + { + uint256[NUMBER_OF_ENTITIES_ZK + 6] memory rhoChallengeElements; + rhoChallengeElements[0] = Fr.unwrap(prevChallenge); + uint256 i; + for (i = 1; i <= NUMBER_OF_ENTITIES_ZK; i++) { + rhoChallengeElements[i] = Fr.unwrap(proof.sumcheckEvaluations[i - 1]); + } + rhoChallengeElements[i] = Fr.unwrap(proof.libraEvaluation); + i += 1; + rhoChallengeElements[i] = proof.libraCommitments[1].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[1].y; + i += 2; + rhoChallengeElements[i] = proof.libraCommitments[2].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[2].y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); + (rho,) = splitChallenge(nextPreviousChallenge); + } + + function generateGeminiRChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr geminiR, Fr nextPreviousChallenge) + { + uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); + gR[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 0; i < logN - 1; i++) { + gR[1 + i * 2] = proof.geminiFoldComms[i].x; + gR[2 + i * 2] = proof.geminiFoldComms[i].y; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); + + (geminiR,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkNuChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr shplonkNu, Fr nextPreviousChallenge) + { + uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1 + 4); + shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 1; i <= logN; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.geminiAEvaluations[i - 1]); + } + + uint256 libraIdx = 0; + for (uint256 i = logN + 1; i <= logN + 4; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.libraPolyEvals[libraIdx]); + libraIdx++; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); + (shplonkNu,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkZChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr shplonkZ, Fr nextPreviousChallenge) + { + uint256[3] memory shplonkZChallengeElements; + shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); + + shplonkZChallengeElements[1] = proof.shplonkQ.x; + shplonkZChallengeElements[2] = proof.shplonkQ.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); + (shplonkZ,) = splitChallenge(nextPreviousChallenge); + } + + function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.ZKProof memory p) { + uint256 boundary = 0x0; + + // Pairing point object + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); + // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 + require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); + p.pairingPointObject[i] = FrLib.from(limb); + boundary += FIELD_ELEMENT_SIZE; + } + + // Gemini masking polynomial commitment (sent first in ZK flavors, right after pairing points) + p.geminiMaskingPoly = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Commitments + p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Lookup / Permutation Helper Commitments + p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[0] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + p.libraSum = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + // Sumcheck univariates + for (uint256 i = 0; i < logN; i++) { + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + } + + // Sumcheck evaluations (includes gemini_masking_poly eval at index 0 for ZK flavors) + for (uint256 i = 0; i < NUMBER_OF_ENTITIES_ZK; i++) { + p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + p.libraEvaluation = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + + p.libraCommitments[1] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[2] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Gemini + // Read gemini fold univariates + for (uint256 i = 0; i < logN - 1; i++) { + p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + } + + // Read gemini a evaluations + for (uint256 i = 0; i < logN; i++) { + p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + for (uint256 i = 0; i < 4; i++) { + p.libraPolyEvals[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + // Shplonk + p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + // KZG + p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + } +} + +library RelationsLib { + struct EllipticParams { + // Points + Fr x_1; + Fr y_1; + Fr x_2; + Fr y_2; + Fr y_3; + Fr x_3; + // push accumulators into memory + Fr x_double_identity; + } + + // Parameters used within the Memory Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct MemParams { + Fr memory_record_check; + Fr partial_record_check; + Fr next_gate_access_type; + Fr record_delta; + Fr index_delta; + Fr adjacent_values_match_if_adjacent_indices_match; + Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; + Fr access_check; + Fr next_gate_access_type_is_boolean; + Fr ROM_consistency_check_identity; + Fr RAM_consistency_check_identity; + Fr timestamp_delta; + Fr RAM_timestamp_check_identity; + Fr memory_identity; + Fr index_is_monotonically_increasing; + } + + // Parameters used within the Non-Native Field Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct NnfParams { + Fr limb_subproduct; + Fr non_native_field_gate_1; + Fr non_native_field_gate_2; + Fr non_native_field_gate_3; + Fr limb_accumulator_1; + Fr limb_accumulator_2; + Fr nnf_identity; + } + + struct PoseidonExternalParams { + Fr s1; + Fr s2; + Fr s3; + Fr s4; + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr t0; + Fr t1; + Fr t2; + Fr t3; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr q_pos_by_scaling; + } + + struct PoseidonInternalParams { + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr u_sum; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr s1; + Fr q_pos_by_scaling; + } + + Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) + uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; + + // Constants for the Non-native Field relation + Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); + Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); + + function accumulateRelationEvaluations( + Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, + Fr powPartialEval + ) internal pure returns (Fr accumulator) { + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; + + // Accumulate all relations in Ultra Honk - each with varying number of subrelations + accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); + + // batch the subrelations with the precomputed alpha powers to obtain the full honk relation + accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); + } + + /** + * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids + * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code + * editors, and thus is noisy. + */ + function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { + return p[uint256(_wire)]; + } + + /** + * Ultra Arithmetic Relation + * + */ + function accumulateArithmeticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + // Relation 0 + Fr q_arith = wire(p, WIRE.Q_ARITH); + { + Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); + + Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; + accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) + + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); + accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); + accum = accum * q_arith; + accum = accum * domainSep; + evals[0] = accum; + } + + // Relation 1 + { + Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); + accum = accum * (q_arith - Fr.wrap(2)); + accum = accum * (q_arith - ONE); + accum = accum * q_arith; + accum = accum * domainSep; + evals[1] = accum; + } + } + + function accumulatePermutationRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr grand_product_numerator; + Fr grand_product_denominator; + + { + Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; + num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); + + grand_product_numerator = num; + } + { + Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; + den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); + + grand_product_denominator = den; + } + + // Contribution 2 + { + Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; + + acc = acc + - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) + * grand_product_denominator); + acc = acc * domainSep; + evals[2] = acc; + } + + // Contribution 3 + { + Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; + evals[3] = acc; + } + } + + function accumulateLogDerivativeLookupRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr table_term; + Fr lookup_term; + + // Calculate the write term (the table accumulation) + // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) + + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); + } + + // Calculate the read term + // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); + Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); + Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); + + lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) + + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); + } + + Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; + Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; + + Fr inverse_exists_xor = + wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) + - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); + + // Inverse calculated correctly relation + Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; + accumulatorNone = accumulatorNone * domainSep; + + // Inverse + Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; + + Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); + + Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; + + evals[4] = accumulatorNone; + evals[5] = accumulatorOne; + evals[6] = read_tag_boolean_relation * domainSep; + } + + function accumulateDeltaRangeRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr minus_one = ZERO - ONE; + Fr minus_two = ZERO - Fr.wrap(2); + Fr minus_three = ZERO - Fr.wrap(3); + + // Compute wire differences + Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); + Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); + Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); + Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); + + // Contribution 6 + { + Fr acc = delta_1; + acc = acc * (delta_1 + minus_one); + acc = acc * (delta_1 + minus_two); + acc = acc * (delta_1 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[7] = acc; + } + + // Contribution 7 + { + Fr acc = delta_2; + acc = acc * (delta_2 + minus_one); + acc = acc * (delta_2 + minus_two); + acc = acc * (delta_2 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[8] = acc; + } + + // Contribution 8 + { + Fr acc = delta_3; + acc = acc * (delta_3 + minus_one); + acc = acc * (delta_3 + minus_two); + acc = acc * (delta_3 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[9] = acc; + } + + // Contribution 9 + { + Fr acc = delta_4; + acc = acc * (delta_4 + minus_one); + acc = acc * (delta_4 + minus_two); + acc = acc * (delta_4 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[10] = acc; + } + } + + function accumulateEllipticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + EllipticParams memory ep; + ep.x_1 = wire(p, WIRE.W_R); + ep.y_1 = wire(p, WIRE.W_O); + + ep.x_2 = wire(p, WIRE.W_L_SHIFT); + ep.y_2 = wire(p, WIRE.W_4_SHIFT); + ep.y_3 = wire(p, WIRE.W_O_SHIFT); + ep.x_3 = wire(p, WIRE.W_R_SHIFT); + + Fr q_sign = wire(p, WIRE.Q_L); + Fr q_is_double = wire(p, WIRE.Q_M); + + // Contribution 10 point addition, x-coordinate check + // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 + Fr x_diff = (ep.x_2 - ep.x_1); + Fr y1_sqr = (ep.y_1 * ep.y_1); + { + // Move to top + Fr partialEval = domainSep; + + Fr y2_sqr = (ep.y_2 * ep.y_2); + Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; + Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); + x_add_identity = x_add_identity * x_diff * x_diff; + x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; + + evals[11] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 11 point addition, x-coordinate check + // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 + { + Fr y1_plus_y3 = ep.y_1 + ep.y_3; + Fr y_diff = ep.y_2 * q_sign - ep.y_1; + Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; + evals[12] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 10 point doubling, x-coordinate check + // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 + // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 + { + Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; + Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; + y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; + Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); + + // NOTE: pushed into memory (stack >:'( ) + ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; + + Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + evals[11] = evals[11] + acc; + } + + // Contribution 11 point doubling, y-coordinate check + // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 + { + Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; + Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); + evals[12] = evals[12] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + } + } + + function accumulateMemoryRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + MemParams memory ap; + + // Compute eta powers locally + Fr eta_two = rp.eta * rp.eta; + Fr eta_three = eta_two * rp.eta; + + /** + * MEMORY + * + * A RAM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) + * * v: `value` of memory cell being accessed + * * a: `access` type of record. read: 0 = read, 1 = write + * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three + * + * A ROM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three + * + * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + + * selectors, depending on whether the gate is a RAM read/write or a ROM read + * + * | gate type | i | v2/t | v | a | r | + * | --------- | -- | ----- | -- | -- | -- | + * | ROM | w1 | w2 | w3 | -- | w4 | + * | RAM | w1 | w2 | w3 | qc | w4 | + * + * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on + * `w2` to fix its value) + * + * + */ + + /** + * Memory Record Check + * Partial degree: 1 + * Total degree: 4 + * + * A ROM/ROM access gate can be evaluated with the identity: + * + * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 + * + * For ROM gates, qc = 0 + */ + ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); + ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); + ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 + ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); + + /** + * Contribution 13 & 14 + * ROM Consistency Check + * Partial degree: 1 + * Total degree: 4 + * + * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of + * records that are sorted. + * + * We apply the following checks for the sorted records: + * + * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 + * 2. index values for adjacent records are monotonically increasing + * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} + * + */ + ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); + ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); + + ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 + + ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 + + evals[14] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + evals[15] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + + ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 + + /** + * Contributions 15,16,17 + * RAM Consistency Check + * + * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` + * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. + * This is validated by requiring `access` to be boolean + * + * For two adjacent entries in the sorted list if _both_ + * A) index values match + * B) adjacent access value is 0 (i.e. next gate is a READ) + * then + * C) both values must match. + * The gate boolean check is + * (A && B) => C === !(A && B) || C === !A || !B || C + * + * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized + * with a WRITE operation. + */ + Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 + ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 + + // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta + // deg 1 or 4 + ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); + ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; + + Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); + ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = + (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 + + // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the + // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't + // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access + // type is correct, to cover this edge case + // deg 2 or 4 + ap.next_gate_access_type_is_boolean = + ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; + + // Putting it all together... + evals[16] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation + * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 + evals[17] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 + evals[18] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 6 + + ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 + + /** + * RAM Timestamp Consistency Check + * + * | w1 | w2 | w3 | w4 | + * | index | timestamp | timestamp_check | -- | + * + * Let delta_index = index_{i + 1} - index_{i} + * + * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i + * Else timestamp_check = 0 + */ + ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); + ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg 3 + + /** + * Complete Contribution 12 + * The complete RAM/ROM memory identity + * Partial degree: + */ + ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 + ap.memory_identity = + ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg 4 + ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 or 6 + ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 + + // (deg 3 or 9) + (deg 4) + (deg 3) + ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 + evals[13] = ap.memory_identity; + } + + function accumulateNnfRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + NnfParams memory ap; + + /** + * Contribution 12 + * Non native field arithmetic gate 2 + * deg 4 + * + * _ _ + * / _ _ _ 14 \ + * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | + * \_ _/ + * + * + */ + ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); + ap.non_native_field_gate_2 = + (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); + + ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; + ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); + ap.non_native_field_gate_1 = ap.limb_subproduct; + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); + + ap.non_native_field_gate_3 = ap.limb_subproduct; + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); + + Fr non_native_field_identity = + ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; + non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); + + // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm + // deg 2 + ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); + ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); + + // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm + // deg 2 + ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); + ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); + + Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; + limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 + + ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; + ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); + evals[19] = ap.nnf_identity; + } + + function accumulatePoseidonExternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonExternalParams memory ep; + + ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); + ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); + ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); + + ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; + ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; + ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; + ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; + // matrix mul v = M_E * u with 14 additions + ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 + ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 + ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 + // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 + ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 + // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 + ep.v4 = ep.t1 + ep.t1; + ep.v4 = ep.v4 + ep.v4 + ep.t3; + // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 + ep.v2 = ep.t0 + ep.t0; + ep.v2 = ep.v2 + ep.v2 + ep.t2; + // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 + ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 + ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 + + ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; + evals[20] = evals[20] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); + + evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); + + evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); + + evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + function accumulatePoseidonInternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonInternalParams memory ip; + + Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ + FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), + FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), + FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), + FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) + ]; + + // add round constants + ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + + // apply s-box round + ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; + ip.u2 = wire(p, WIRE.W_R); + ip.u3 = wire(p, WIRE.W_O); + ip.u4 = wire(p, WIRE.W_4); + + // matrix mul with v = M_I * u 4 muls and 7 additions + ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; + + ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; + + ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; + evals[24] = evals[24] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); + + ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; + evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); + + ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; + evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); + + ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; + evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + // Batch subrelation evaluations using precomputed powers of alpha + // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array + function scaleAndBatchSubrelations( + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges + ) internal pure returns (Fr accumulator) { + accumulator = evaluations[0]; + + for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { + accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; + } + } +} + +library CommitmentSchemeLib { + using FrLib for Fr; + + // Avoid stack too deep + struct ShpleminiIntermediates { + Fr unshiftedScalar; + Fr shiftedScalar; + Fr unshiftedScalarNeg; + Fr shiftedScalarNeg; + // Scalar to be multiplied by [1]₁ + Fr constantTermAccumulator; + // Accumulator for powers of rho + Fr batchingChallenge; + // Linear combination of multilinear (sumcheck) evaluations and powers of rho + Fr batchedEvaluation; + Fr[4] denominators; + Fr[4] batchingScalars; + // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr posInvertedDenominator; + // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr negInvertedDenominator; + // ν^{2i} * 1/(z - r^{2^i}) + Fr scalingFactorPos; + // ν^{2i+1} * 1/(z + r^{2^i}) + Fr scalingFactorNeg; + // Fold_i(r^{2^i}) reconstructed by Verifier + Fr[] foldPosEvaluations; + } + + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 + function computeFoldPosEvaluations( + Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, + Fr batchedEvalAccumulator, + Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, + Fr[] memory geminiEvalChallengePowers, + uint256 logSize + ) internal view returns (Fr[] memory) { + Fr[] memory foldPosEvaluations = new Fr[](logSize); + for (uint256 i = logSize; i > 0; --i) { + Fr challengePower = geminiEvalChallengePowers[i - 1]; + Fr u = sumcheckUChallenges[i - 1]; + + Fr batchedEvalRoundAcc = ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] + * (challengePower * (ONE - u) - u)); + // Divide by the denominator + batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); + + batchedEvalAccumulator = batchedEvalRoundAcc; + foldPosEvaluations[i - 1] = batchedEvalRoundAcc; + } + return foldPosEvaluations; + } + + function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { + Fr[] memory squares = new Fr[](logN); + squares[0] = r; + for (uint256 i = 1; i < logN; ++i) { + squares[i] = squares[i - 1].sqr(); + } + return squares; + } +} + +uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // EC group order. F_q + +// Fr utility + +function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { + scalar = FrLib.fromBytes32(bytes32(proofSection)); +} + +// EC Point utilities +function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { + uint256 x = uint256(bytes32(proofSection[0x00:0x20])); + uint256 y = uint256(bytes32(proofSection[0x20:0x40])); + require(x < Q && y < Q, Errors.ValueGeGroupOrder()); + + // Reject the point at infinity (0,0). EVM precompiles silently treat (0,0) + // as the identity element, which could zero out commitments. + // On-curve validation (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles + // per EIP-196, so we only need to catch this special case here. + require((x | y) != 0, Errors.PointAtInfinity()); + + point = Honk.G1Point({x: x, y: y}); +} + +function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { + // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. + if (point.y != 0) { + point.y = Q - point.y; + } + return point; +} + +/** + * Convert the pairing points to G1 points. + * + * The pairing points are serialised as an array of 2 limbs representing two points + * (P0 and P1, used for lhs and rhs of pairing operation). + * + * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. + * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] + * + * @param pairingPoints The pairing points to convert. + * @return lhs P0 point + * @return rhs P1 point + */ +function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) + pure + returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) +{ + // P0 (lhs): x = lo | (hi << 136) + uint256 lhsX = Fr.unwrap(pairingPoints[0]); + lhsX |= Fr.unwrap(pairingPoints[1]) << 136; + + uint256 lhsY = Fr.unwrap(pairingPoints[2]); + lhsY |= Fr.unwrap(pairingPoints[3]) << 136; + + // P1 (rhs): x = lo | (hi << 136) + uint256 rhsX = Fr.unwrap(pairingPoints[4]); + rhsX |= Fr.unwrap(pairingPoints[5]) << 136; + + uint256 rhsY = Fr.unwrap(pairingPoints[6]); + rhsY |= Fr.unwrap(pairingPoints[7]) << 136; + + // Reconstructed coordinates must be < Q to prevent malleability. + // Without this, two different limb encodings could map to the same curve point + // (via mulmod reduction in on-curve checks) but produce different transcript hashes. + require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); + + lhs.x = lhsX; + lhs.y = lhsY; + rhs.x = rhsX; + rhs.y = rhsY; +} + +/** + * Hash the pairing inputs from the present verification context with those extracted from the public inputs. + * + * @param proofPairingPoints Pairing points from the proof - (public inputs). + * @param accLhs Accumulator point for the left side - result of shplemini. + * @param accRhs Accumulator point for the right side - result of shplemini. + * @return recursionSeparator The recursion separator - generated from hashing the above. + */ +function generateRecursionSeparator( + Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, + Honk.G1Point memory accLhs, + Honk.G1Point memory accRhs +) pure returns (Fr recursionSeparator) { + // hash the proof aggregated X + // hash the proof aggregated Y + // hash the accum X + // hash the accum Y + + (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); + + uint256[8] memory recursionSeparatorElements; + + // Proof points + recursionSeparatorElements[0] = proofLhs.x; + recursionSeparatorElements[1] = proofLhs.y; + recursionSeparatorElements[2] = proofRhs.x; + recursionSeparatorElements[3] = proofRhs.y; + + // Accumulator points + recursionSeparatorElements[4] = accLhs.x; + recursionSeparatorElements[5] = accLhs.y; + recursionSeparatorElements[6] = accRhs.x; + recursionSeparatorElements[7] = accRhs.y; + + recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); +} + +/** + * G1 Mul with Separator + * Using the ecAdd and ecMul precompiles + * + * @param basePoint The point to multiply. + * @param other The other point to add. + * @param recursionSeperator The separator to use for the multiplication. + * @return `(recursionSeperator * basePoint) + other`. + */ +function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) + view + returns (Honk.G1Point memory) +{ + Honk.G1Point memory result; + + result = ecMul(recursionSeperator, basePoint); + result = ecAdd(result, other); + + return result; +} + +/** + * G1 Mul + * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. + * + * @param value The value to multiply the point by. + * @param point The point to multiply. + * @return result The result of the multiplication. + */ +function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write the point into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | point.x + // free + 0x20| point.y + mstore(free, mload(point)) + mstore(add(free, 0x20), mload(add(point, 0x20))) + // Write the scalar into memory (one 32 byte word) + // Memory layout: + // Address | value + // free + 0x40| value + mstore(add(free, 0x40), value) + + // Call the ecMul precompile, it takes in the following + // [point.x, point.y, scalar], and returns the result back into the free memory location. + let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) + if iszero(success) { + revert(0, 0) + } + // Copy the result of the multiplication back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x60)) + } + + return result; +} + +/** + * G1 Add + * Takes two G1 points and uses the ecAdd precompile to return the result. + * + * @param lhs The left hand side of the addition. + * @param rhs The right hand side of the addition. + * @return result The result of the addition. + */ +function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write lhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | lhs.x + // free + 0x20| lhs.y + mstore(free, mload(lhs)) + mstore(add(free, 0x20), mload(add(lhs, 0x20))) + + // Write rhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free + 0x40| rhs.x + // free + 0x60| rhs.y + mstore(add(free, 0x40), mload(rhs)) + mstore(add(free, 0x60), mload(add(rhs, 0x20))) + + // Call the ecAdd precompile, it takes in the following + // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. + let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) + if iszero(success) { revert(0, 0) } + + // Copy the result of the addition back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x80)) + } + + return result; +} + +function rejectPointAtInfinity(Honk.G1Point memory point) pure { + require((point.x | point.y) != 0, Errors.PointAtInfinity()); +} + +/** + * Check if pairing point limbs are all zero (default/infinity). + * Default pairing points indicate no recursive verification occurred. + */ +function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { + uint256 acc = 0; + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + acc |= Fr.unwrap(pairingPoints[i]); + } + return acc == 0; +} + +function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { + bytes memory input = abi.encodePacked( + rhs.x, + rhs.y, + // Fixed G2 point + uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), + uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), + uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), + uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), + lhs.x, + lhs.y, + // G2 point from VK + uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), + uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), + uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), + uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) + ); + + (bool success, bytes memory result) = address(0x08).staticcall(input); + decodedResult = success && abi.decode(result, (bool)); +} + +abstract contract BaseZKHonkVerifier is IVerifier { + using FrLib for Fr; + + struct PairingInputs { + Honk.G1Point P_0; + Honk.G1Point P_1; + } + + struct SmallSubgroupIpaIntermediates { + Fr[SUBGROUP_SIZE] challengePolyLagrange; + Fr challengePolyEval; + Fr lagrangeFirst; + Fr lagrangeLast; + Fr rootPower; + Fr[SUBGROUP_SIZE] denominators; // this has to disappear + Fr diff; + } + + // Constants for proof length calculation (matching UltraKeccakZKFlavor) + uint256 internal constant NUM_WITNESS_ENTITIES = 8 + NUM_MASKING_POLYNOMIALS; + uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points + uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements + uint256 internal constant NUM_LIBRA_EVALUATIONS = 4; // libra evaluations + + uint256 internal constant LIBRA_COMMITMENTS = 3; + uint256 internal constant LIBRA_EVALUATIONS = 4; + uint256 internal constant LIBRA_UNIVARIATES_LENGTH = 9; + + uint256 internal constant SHIFTED_COMMITMENTS_START = 30; + uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; + + uint256 internal immutable $N; + uint256 internal immutable $LOG_N; + uint256 internal immutable $VK_HASH; + uint256 internal immutable $NUM_PUBLIC_INPUTS; + uint256 internal immutable $MSMSize; + + constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { + $N = _N; + $LOG_N = _logN; + $VK_HASH = _vkHash; + $NUM_PUBLIC_INPUTS = _numPublicInputs; + $MSMSize = NUMBER_UNSHIFTED_ZK + _logN + LIBRA_COMMITMENTS + 2; + } + + function verify(bytes calldata proof, bytes32[] calldata publicInputs) + public + view + override + returns (bool verified) + { + // Calculate expected proof size based on $LOG_N + uint256 expectedProofSize = calculateProofSize($LOG_N); + + // Check the received proof is the expected size where each field element is 32 bytes + require( + proof.length == expectedProofSize, Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize) + ); + + Honk.VerificationKey memory vk = loadVerificationKey(); + Honk.ZKProof memory p = ZKTranscriptLib.loadProof(proof, $LOG_N); + + require(publicInputs.length == vk.publicInputsSize - PAIRING_POINTS_SIZE, Errors.PublicInputsLengthWrong()); + + // Generate the fiat shamir challenges for the whole protocol + ZKTranscript memory t = + ZKTranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); + + // Derive public input delta + t.relationParameters.publicInputsDelta = computePublicInputDelta( + publicInputs, + p.pairingPointObject, + t.relationParameters.beta, + t.relationParameters.gamma, /*pubInputsOffset=*/ + 1 + ); + + // Sumcheck + require(verifySumcheck(p, t), Errors.SumcheckFailed()); + require(verifyShplemini(p, vk, t), Errors.ShpleminiFailed()); + + verified = true; + } + + function computePublicInputDelta( + bytes32[] memory publicInputs, + Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, + Fr beta, + Fr gamma, + uint256 offset + ) internal view returns (Fr publicInputDelta) { + Fr numerator = Fr.wrap(1); + Fr denominator = Fr.wrap(1); + + Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); + Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); + + { + for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { + Fr pubInput = FrLib.fromBytes32(publicInputs[i]); + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + Fr pubInput = pairingPointObject[i]; + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + } + + // Fr delta = numerator / denominator; // TOOO: batch invert later? + publicInputDelta = FrLib.div(numerator, denominator); + } + + function verifySumcheck(Honk.ZKProof memory proof, ZKTranscript memory tp) internal view returns (bool verified) { + Fr roundTargetSum = tp.libraChallenge * proof.libraSum; // default 0 + Fr powPartialEvaluation = Fr.wrap(1); + + // We perform sumcheck reductions over log n rounds ( the multivariate degree ) + for (uint256 round; round < $LOG_N; ++round) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; + Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; + require(totalSum == roundTargetSum, Errors.SumcheckFailed()); + + Fr roundChallenge = tp.sumCheckUChallenges[round]; + + // Update the round target for the next rounf + roundTargetSum = computeNextTargetSum(roundUnivariate, roundChallenge); + powPartialEvaluation = + powPartialEvaluation * (Fr.wrap(1) + roundChallenge * (tp.gateChallenges[round] - Fr.wrap(1))); + } + + // Last round + // For ZK flavors: sumcheckEvaluations has 42 elements + // Index 0 is gemini_masking_poly, indices 1-41 are the regular entities used in relations + Fr[NUMBER_OF_ENTITIES] memory relationsEvaluations; + for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { + relationsEvaluations[i] = proof.sumcheckEvaluations[i + NUM_MASKING_POLYNOMIALS]; // Skip gemini_masking_poly at index 0 + } + Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( + relationsEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation + ); + + Fr evaluation = Fr.wrap(1); + for (uint256 i = 2; i < $LOG_N; i++) { + evaluation = evaluation * tp.sumCheckUChallenges[i]; + } + + grandHonkRelationSum = + grandHonkRelationSum * (Fr.wrap(1) - evaluation) + proof.libraEvaluation * tp.libraChallenge; + verified = (grandHonkRelationSum == roundTargetSum); + } + + // Return the new target sum for the next sumcheck round + function computeNextTargetSum(Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) + internal + view + returns (Fr targetSum) + { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000240), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80) + ]; + + // To compute the next target sum, we evaluate the given univariate at a point u (challenge). + + // Performing Barycentric evaluations + // Compute B(x) + Fr numeratorValue = Fr.wrap(1); + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); + } + + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + denominatorInverses[i] = FrLib.invert(BARYCENTRIC_LAGRANGE_DENOMINATORS[i] * (roundChallenge - Fr.wrap(i))); + } + + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + targetSum = targetSum + roundUnivariates[i] * denominatorInverses[i]; + } + + // Scale the sum by the value of B(x) + targetSum = targetSum * numeratorValue; + } + + function verifyShplemini(Honk.ZKProof memory proof, Honk.VerificationKey memory vk, ZKTranscript memory tp) + internal + view + returns (bool verified) + { + CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack + + // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size + Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); + // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings + Fr[] memory scalars = new Fr[]($MSMSize); + Honk.G1Point[] memory commitments = new Honk.G1Point[]($MSMSize); + + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); + + mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); + mem.shiftedScalar = + tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); + + scalars[0] = Fr.wrap(1); + commitments[0] = proof.shplonkQ; + + /* Batch multivariate opening claims, shifted and unshifted + * The vector of scalars is populated as follows: + * \f[ + * \left( + * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) + * \right) + * \f] + * + * The following vector is concatenated to the vector of commitments: + * \f[ + * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} + * \f] + * + * Simultaneously, the evaluation of the multilinear polynomial + * \f[ + * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} + * \f] + * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. + * + * This approach minimizes the number of iterations over the commitments to multilinear polynomials + * and eliminates the need to store the powers of \f$ \rho \f$. + */ + // For ZK flavors: evaluations array is [gemini_masking_poly, qm, qc, ql, qr, ...] + // Start batching challenge at 1, not rho, to match non-ZK pattern + mem.batchingChallenge = Fr.wrap(1); + mem.batchedEvaluation = Fr.wrap(0); + + mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); + mem.shiftedScalarNeg = mem.shiftedScalar.neg(); + + // Process all NUMBER_UNSHIFTED_ZK evaluations (includes gemini_masking_poly at index 0) + for (uint256 i = 1; i <= NUMBER_UNSHIFTED_ZK; ++i) { + scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; + mem.batchedEvaluation = mem.batchedEvaluation + + (proof.sumcheckEvaluations[i - NUM_MASKING_POLYNOMIALS] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + // g commitments are accumulated at r + // For each of the to be shifted commitments perform the shift in place by + // adding to the unshifted value. + // We do so, as the values are to be used in batchMul later, and as + // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. + // Applied to w1, w2, w3, w4 and zPerm + for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { + uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; + uint256 evaluationOff = i + NUMBER_UNSHIFTED_ZK; + + scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); + mem.batchedEvaluation = + mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + + commitments[1] = proof.geminiMaskingPoly; + + commitments[2] = vk.qm; + commitments[3] = vk.qc; + commitments[4] = vk.ql; + commitments[5] = vk.qr; + commitments[6] = vk.qo; + commitments[7] = vk.q4; + commitments[8] = vk.qLookup; + commitments[9] = vk.qArith; + commitments[10] = vk.qDeltaRange; + commitments[11] = vk.qElliptic; + commitments[12] = vk.qMemory; + commitments[13] = vk.qNnf; + commitments[14] = vk.qPoseidon2External; + commitments[15] = vk.qPoseidon2Internal; + commitments[16] = vk.s1; + commitments[17] = vk.s2; + commitments[18] = vk.s3; + commitments[19] = vk.s4; + commitments[20] = vk.id1; + commitments[21] = vk.id2; + commitments[22] = vk.id3; + commitments[23] = vk.id4; + commitments[24] = vk.t1; + commitments[25] = vk.t2; + commitments[26] = vk.t3; + commitments[27] = vk.t4; + commitments[28] = vk.lagrangeFirst; + commitments[29] = vk.lagrangeLast; + + // Accumulate proof points + commitments[30] = proof.w1; + commitments[31] = proof.w2; + commitments[32] = proof.w3; + commitments[33] = proof.w4; + commitments[34] = proof.zPerm; + commitments[35] = proof.lookupInverses; + commitments[36] = proof.lookupReadCounts; + commitments[37] = proof.lookupReadTags; + + /* Batch gemini claims from the prover + * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from + * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars + * + * 1. Moves the vector + * \f[ + * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) + * \f] + * to the 'commitments' vector. + * + * 2. Computes the scalars: + * \f[ + * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} + * \f] + * and places them into the 'scalars' vector. + * + * 3. Accumulates the summands of the constant term: + * \f[ + * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} + * \f] + * and adds them to the 'constant_term_accumulator'. + */ + + // Add contributions from A₀(r) and A₀(-r) to constant_term_accumulator: + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 + Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( + tp.sumCheckUChallenges, + mem.batchedEvaluation, + proof.geminiAEvaluations, + powers_of_evaluation_challenge, + $LOG_N + ); + + mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; + mem.constantTermAccumulator = + mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); + + mem.batchingChallenge = tp.shplonkNu.sqr(); + uint256 boundary = NUMBER_UNSHIFTED_ZK + 1; + + // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; + // Compute scalar multipliers for each fold commitment + for (uint256 i = 0; i < $LOG_N - 1; ++i) { + bool dummy_round = i >= ($LOG_N - 1); + + if (!dummy_round) { + // Update inverted denominators + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); + + // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] + mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; + mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; + scalars[boundary + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); + + // Accumulate the const term contribution given by + // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) + Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; + accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; + mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; + } + // Update the running power of v + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; + + commitments[boundary + i] = proof.geminiFoldComms[i]; + } + + boundary += $LOG_N - 1; + + // Finalize the batch opening claim + mem.denominators[0] = Fr.wrap(1).div(tp.shplonkZ - tp.geminiR); + mem.denominators[1] = Fr.wrap(1).div(tp.shplonkZ - SUBGROUP_GENERATOR * tp.geminiR); + mem.denominators[2] = mem.denominators[0]; + mem.denominators[3] = mem.denominators[0]; + + for (uint256 i = 0; i < LIBRA_EVALUATIONS; i++) { + Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; + mem.batchingScalars[i] = scalingFactor.neg(); + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; + mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i]; + } + scalars[boundary] = mem.batchingScalars[0]; + scalars[boundary + 1] = mem.batchingScalars[1] + mem.batchingScalars[2]; + scalars[boundary + 2] = mem.batchingScalars[3]; + + for (uint256 i = 0; i < LIBRA_COMMITMENTS; i++) { + commitments[boundary++] = proof.libraCommitments[i]; + } + + commitments[boundary] = Honk.G1Point({x: 1, y: 2}); + scalars[boundary++] = mem.constantTermAccumulator; + + require( + checkEvalsConsistency(proof.libraPolyEvals, tp.geminiR, tp.sumCheckUChallenges, proof.libraEvaluation), + Errors.ConsistencyCheckFailed() + ); + + Honk.G1Point memory quotient_commitment = proof.kzgQuotient; + + commitments[boundary] = quotient_commitment; + scalars[boundary] = tp.shplonkZ; // evaluation challenge + + PairingInputs memory pair; + pair.P_0 = batchMul(commitments, scalars); + pair.P_1 = negateInplace(quotient_commitment); + + // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) + if (!arePairingPointsDefault(proof.pairingPointObject)) { + Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, pair.P_0, pair.P_1); + (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = + convertPairingPointsToG1(proof.pairingPointObject); + + // Validate the points from the proof are on the curve + rejectPointAtInfinity(P_0_other); + rejectPointAtInfinity(P_1_other); + + // accumulate with aggregate points in proof + pair.P_0 = mulWithSeperator(pair.P_0, P_0_other, recursionSeparator); + pair.P_1 = mulWithSeperator(pair.P_1, P_1_other, recursionSeparator); + } + + return pairing(pair.P_0, pair.P_1); + } + + function checkEvalsConsistency( + Fr[LIBRA_EVALUATIONS] memory libraPolyEvals, + Fr geminiR, + Fr[CONST_PROOF_SIZE_LOG_N] memory uChallenges, + Fr libraEval + ) internal view returns (bool check) { + Fr one = Fr.wrap(1); + Fr vanishingPolyEval = geminiR.pow(SUBGROUP_SIZE) - one; + require(vanishingPolyEval != Fr.wrap(0), Errors.GeminiChallengeInSubgroup()); + + SmallSubgroupIpaIntermediates memory mem; + mem.challengePolyLagrange[0] = one; + for (uint256 round = 0; round < $LOG_N; round++) { + uint256 currIdx = 1 + LIBRA_UNIVARIATES_LENGTH * round; + mem.challengePolyLagrange[currIdx] = one; + for (uint256 idx = currIdx + 1; idx < currIdx + LIBRA_UNIVARIATES_LENGTH; idx++) { + mem.challengePolyLagrange[idx] = mem.challengePolyLagrange[idx - 1] * uChallenges[round]; + } + } + + mem.rootPower = one; + mem.challengePolyEval = Fr.wrap(0); + for (uint256 idx = 0; idx < SUBGROUP_SIZE; idx++) { + mem.denominators[idx] = mem.rootPower * geminiR - one; + mem.denominators[idx] = mem.denominators[idx].invert(); + mem.challengePolyEval = mem.challengePolyEval + mem.challengePolyLagrange[idx] * mem.denominators[idx]; + mem.rootPower = mem.rootPower * SUBGROUP_GENERATOR_INVERSE; + } + + Fr numerator = vanishingPolyEval * Fr.wrap(SUBGROUP_SIZE).invert(); + mem.challengePolyEval = mem.challengePolyEval * numerator; + mem.lagrangeFirst = mem.denominators[0] * numerator; + mem.lagrangeLast = mem.denominators[SUBGROUP_SIZE - 1] * numerator; + + mem.diff = mem.lagrangeFirst * libraPolyEvals[2]; + + mem.diff = mem.diff + (geminiR - SUBGROUP_GENERATOR_INVERSE) + * (libraPolyEvals[1] - libraPolyEvals[2] - libraPolyEvals[0] * mem.challengePolyEval); + mem.diff = mem.diff + mem.lagrangeLast * (libraPolyEvals[2] - libraEval) - vanishingPolyEval * libraPolyEvals[3]; + + check = mem.diff == Fr.wrap(0); + } + + // This implementation is the same as above with different constants + function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) + internal + view + returns (Honk.G1Point memory result) + { + uint256 limit = $MSMSize; + + // Validate all points are on the curve + for (uint256 i = 0; i < limit; ++i) { + rejectPointAtInfinity(base[i]); + } + + bool success = true; + assembly { + let free := mload(0x40) + + let count := 0x01 + for {} lt(count, add(limit, 1)) { count := add(count, 1) } { + // Get loop offsets + let base_base := add(base, mul(count, 0x20)) + let scalar_base := add(scalars, mul(count, 0x20)) + + mstore(add(free, 0x40), mload(mload(base_base))) + mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) + // Add scalar + mstore(add(free, 0x80), mload(scalar_base)) + + success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) + // accumulator = accumulator + accumulator_2 + success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) + } + + // Return the result + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + } + + require(success, Errors.ShpleminiFailed()); + } + + // Calculate proof size based on log_n (matching UltraKeccakZKFlavor formula) + function calculateProofSize(uint256 logN) internal pure returns (uint256) { + // Witness and Libra commitments + uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments + proofLength += NUM_ELEMENTS_COMM * 3; // Libra concat, grand sum, quotient comms + Gemini masking + + // Sumcheck + proofLength += logN * ZK_BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates + proofLength += NUMBER_OF_ENTITIES_ZK * NUM_ELEMENTS_FR; // sumcheck evaluations + + // Libra and Gemini + proofLength += NUM_ELEMENTS_FR * 2; // Libra sum, claimed eval + proofLength += logN * NUM_ELEMENTS_FR; // Gemini a evaluations + proofLength += NUM_LIBRA_EVALUATIONS * NUM_ELEMENTS_FR; // libra evaluations + + // PCS commitments + proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments + proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments + + // Pairing points + proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs + + return proofLength * 32; + } + + function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); +} + +contract HonkVerifier is BaseZKHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { + function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { + return HonkVerificationKey.loadVerificationKey(); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/TransferVerifier.sol b/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/TransferVerifier.sol new file mode 100644 index 0000000..f2e636f --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/TransferVerifier.sol @@ -0,0 +1,2460 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec +pragma solidity >=0.8.21; + +uint256 constant N = 2097152; +uint256 constant LOG_N = 21; +uint256 constant NUMBER_OF_PUBLIC_INPUTS = 19; +uint256 constant VK_HASH = 0x2b171a6938ad8443851a3bed5c956b93338b74db96883c72867be4da5850d966; +library HonkVerificationKey { + function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { + Honk.VerificationKey memory vk = Honk.VerificationKey({ + circuitSize: uint256(2097152), + logCircuitSize: uint256(21), + publicInputsSize: uint256(19), + ql: Honk.G1Point({ + x: uint256(0x1f92017897c0b56fff98b9be811905452145c0dd0418148ab46700f507f8fbbf), + y: uint256(0x1d085b59a91d698e2cd3ef92ab8b0fddfd00d617ce735d6c3b48611ffdcbc19d) + }), + qr: Honk.G1Point({ + x: uint256(0x17cd1b65dc2acaf45d8c96fc3304ad7118ba8fe0f6f579f6b19f0014017d00e6), + y: uint256(0x044ccb4c954eeb4485b5aa5bc97020b063e16366149eb6c1a036fdb62de479ef) + }), + qo: Honk.G1Point({ + x: uint256(0x094f4ac60e5754af350d587f874189cbee479c707c1e08ff52bb9563e19af551), + y: uint256(0x0a2546262db4f0a6dc5c164e212ffa1276bacb5c4f53fc6484dd8a4863cdbf9b) + }), + q4: Honk.G1Point({ + x: uint256(0x297423b1c7330710ea840312f1d162c60f6e100861d46e3fbba2c302555bb5b9), + y: uint256(0x2b8659e6b5bdffc0bd9990ebd610e4bae47ce1e41dfe390ff01ca4560f9cdae5) + }), + qm: Honk.G1Point({ + x: uint256(0x0ba5aa566b2559c3822543e77ba6723092e5da6b32e38c058ec859d186c4e33f), + y: uint256(0x1fc07306c658150924d9c89b0a3c6f79fbc9d0f80cf93fc10c8536d8801ac9d8) + }), + qc: Honk.G1Point({ + x: uint256(0x2681722c0bbefe052906992671dcff98f370bdb83bd1cd8fabc841ec0e3e4c0d), + y: uint256(0x268019488922ec4257ef1c1321231186c28eac1406cbf666248c1d02f4fcca9b) + }), + qLookup: Honk.G1Point({ + x: uint256(0x0073e7c223dd4f3e4734c4d9d9c9df394bd2eee2e12bac2fc49429a0443ec8b0), + y: uint256(0x20fac57db30195c2427a75a4d67231c1d1c74c8f84f009ab21d3c88e9657403d) + }), + qArith: Honk.G1Point({ + x: uint256(0x1f51d33f8654e4319eb9f18262498cc9bb52b4a7aac456ba4f691f1b25df7e93), + y: uint256(0x000dc892b3ece92c2215bcd716e2b59d3b06e677d309fad69f17299aa800cb19) + }), + qDeltaRange: Honk.G1Point({ + x: uint256(0x08208a7da8fe594742f274c17821862c73c9244cbb50baf793025aa345997fcb), + y: uint256(0x2afde4b263a1f8762df81cc1a7254c4699ec59947a557920290642af0e45a0ab) + }), + qElliptic: Honk.G1Point({ + x: uint256(0x02b6ea6e78b21bce67814e62a99176016f8b643cb5517116f19812549cf576f0), + y: uint256(0x0fa714346cdc66a4a08f63061a82f754134c2420f8b86623167ea068fd916259) + }), + qMemory: Honk.G1Point({ + x: uint256(0x217b032ac5d124e706a08660c9f461da140296a0f85657ab3d06e4a551c3834c), + y: uint256(0x08281428b3396f7e9de0d5cad12dd7143cfa822ae66707b50cb91cea49b98030) + }), + qNnf: Honk.G1Point({ + x: uint256(0x2efa044d97785b9118edbf3b76d54c992bf01680a205ff4cc0b52985c89b9ca2), + y: uint256(0x260d9bfb7b51297b9d6faae87a64fc9e75571b0cbfcf6b0e693c465eb2347dcb) + }), + qPoseidon2External: Honk.G1Point({ + x: uint256(0x0cc99faf472182c12282945b4abf0ed669efd51e04f7a73b3818bb102f755518), + y: uint256(0x1583d0b0ff97ac3bd6ab457b9e642d391e87f3e25c3e9171fce741d11a829d71) + }), + qPoseidon2Internal: Honk.G1Point({ + x: uint256(0x0b6846339fef05cf9a9331e7c70d5899cdec5f89afd4991d640705e57c5e676c), + y: uint256(0x15c27b3f8868e06cc96094f64c42cb57315d8407392413f94e79d03ea50253d8) + }), + s1: Honk.G1Point({ + x: uint256(0x1c3afd1afd4ffdfdea15ec129d1063247e6567cdbc65e860938d94ae0eedbac0), + y: uint256(0x048b2bc0dd7bc3d825677f03e648ba0d61b6e0ad23fc2d47c4b7db1b10e07b68) + }), + s2: Honk.G1Point({ + x: uint256(0x091245babbf4a657959f8123750cbec4f7babb3d410e162040b788fe5df30ce3), + y: uint256(0x041541389ebefc25bb222e1f96074495fa698d770e45ca20f6613980e6d8564e) + }), + s3: Honk.G1Point({ + x: uint256(0x1e8a93bc1cb6f0d36f9350ce8fcb4ddfbfbec3f840338c5f50de64b9ef6490be), + y: uint256(0x075111c22410d92c5a0406079ff6271ebc2a70cf5b6e9e607a103d047ba6da09) + }), + s4: Honk.G1Point({ + x: uint256(0x177f068d0e2edb5c977e24238e550d3ac2e0198d82f91f390c1e511be6ad0b70), + y: uint256(0x189c800ccaf3bb45f4aacf6dc5654101d9ebf91352f5139a1db4313886bc92de) + }), + t1: Honk.G1Point({ + x: uint256(0x099e3bd5a0a00ab7fe18040105b9b395b5d8b7b4a63b05df652b0d10ef146d26), + y: uint256(0x0015b8d2515d76e2ccec99dcd194592129af3a637f5a622a32440f860d1e2a7f) + }), + t2: Honk.G1Point({ + x: uint256(0x1b917517920bad3d8bc01c9595092a222b888108dc25d1aa450e0b4bc212c37e), + y: uint256(0x305e8992b148eedb22e6e992077a84482141c7ebe42000a1d58ccb74381f6d19) + }), + t3: Honk.G1Point({ + x: uint256(0x16465a5ccbb550cd2c63bd58116fe47c86847618681dc29d8a9363ab7c40e1c3), + y: uint256(0x2e24d420fbf9508ed31de692db477b439973ac12d7ca796d6fe98ca40e6ca6b7) + }), + t4: Honk.G1Point({ + x: uint256(0x043d063b130adfb37342af45d0155a28edd1a7e46c840d9c943fdf45521c64ce), + y: uint256(0x261522c4089330646aff96736194949330952ae74c573d1686d9cb4a00733854) + }), + id1: Honk.G1Point({ + x: uint256(0x2a9f214dc29f03ad5a157987f2939a897078ade85e14994f8ae0677f41e2b5e3), + y: uint256(0x0d84687c11501ba46f40ca095ecdab00ad96f265287a387f51cb7517040c205d) + }), + id2: Honk.G1Point({ + x: uint256(0x1dab41509edd5dda9b9368a8b7face800778b6c578c76f4c427eb0a82d03a26b), + y: uint256(0x1443942ba3e7f2490e2213e1c76bd699d5995c0b35d252799a222651ab851640) + }), + id3: Honk.G1Point({ + x: uint256(0x00e56c4bfb05f98d5e2209b887484695d208072a23a461cc25d7d04f22efcc3b), + y: uint256(0x1f39d5566e01a96a6888d880c1861b21d4667c8f64ed6e1b8d3ed5566009279f) + }), + id4: Honk.G1Point({ + x: uint256(0x248e54eccb41f1ca68b2ab686c1e31426f28cd5a05f4265f0b79f4027a4dee37), + y: uint256(0x30110b365a2ea3990835fd511724138f5ba4d7ae955b5a93f1182cf07c1b1be8) + }), + lagrangeFirst: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000001), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000002) + }), + lagrangeLast: Honk.G1Point({ + x: uint256(0x09c59ad9bf346632fad7dc58a5960c4ba5ff5da3be4db3a6d26c8c2c3c37a2b8), + y: uint256(0x075e628f070529b17d44f2bf788492e013bacf217130139a73a5ad754159740e) + }) + }); + return vk; + } +} + +pragma solidity ^0.8.27; + +interface IVerifier { + function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); +} + +/** + * @notice Library of error codes + * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier + */ +library Errors { + error ValueGeLimbMax(); + error ValueGeGroupOrder(); + error ValueGeFieldOrder(); + + error InvertOfZero(); + error NotPowerOfTwo(); + error ModExpFailed(); + + error ProofLengthWrong(); + error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); + error PublicInputsLengthWrong(); + error SumcheckFailed(); + error ShpleminiFailed(); + + error PointAtInfinity(); + + error ConsistencyCheckFailed(); + error GeminiChallengeInSubgroup(); +} + +type Fr is uint256; + +using {add as +} for Fr global; +using {sub as -} for Fr global; +using {mul as *} for Fr global; + +using {notEqual as !=} for Fr global; +using {equal as ==} for Fr global; + +uint256 constant SUBGROUP_SIZE = 256; +uint256 constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Prime field order +uint256 constant P = MODULUS; +Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); +Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); +Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); +Fr constant ONE = Fr.wrap(1); +Fr constant ZERO = Fr.wrap(0); +// Instantiation + +library FrLib { + bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; + + function invert(Fr value) internal view returns (Fr) { + uint256 v = Fr.unwrap(value); + require(v != 0, Errors.InvertOfZero()); + + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), v) + mstore(add(free, 0x80), sub(MODULUS, 2)) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function pow(Fr base, uint256 v) internal view returns (Fr) { + uint256 b = Fr.unwrap(base); + // Only works for power of 2 + require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), b) + mstore(add(free, 0x80), v) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function div(Fr numerator, Fr denominator) internal view returns (Fr) { + unchecked { + return numerator * invert(denominator); + } + } + + function sqr(Fr value) internal pure returns (Fr) { + unchecked { + return value * value; + } + } + + function unwrap(Fr value) internal pure returns (uint256) { + unchecked { + return Fr.unwrap(value); + } + } + + function neg(Fr value) internal pure returns (Fr) { + unchecked { + return Fr.wrap(MODULUS - Fr.unwrap(value)); + } + } + + function from(uint256 value) internal pure returns (Fr) { + unchecked { + require(value < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(value); + } + } + + function fromBytes32(bytes32 value) internal pure returns (Fr) { + unchecked { + uint256 v = uint256(value); + require(v < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(v); + } + } + + function toBytes32(Fr value) internal pure returns (bytes32) { + unchecked { + return bytes32(Fr.unwrap(value)); + } + } +} + +// Free functions +function add(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function mul(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function sub(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); + } +} + +function notEqual(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) != Fr.unwrap(b); + } +} + +function equal(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) == Fr.unwrap(b); + } +} + +uint256 constant CONST_PROOF_SIZE_LOG_N = 25; + +uint256 constant NUMBER_OF_SUBRELATIONS = 28; +uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; +uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; +uint256 constant NUMBER_OF_ENTITIES = 41; +// The number of entities added for ZK (gemini_masking_poly) +uint256 constant NUM_MASKING_POLYNOMIALS = 1; +uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_UNSHIFTED = 36; +uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_TO_BE_SHIFTED = 5; +uint256 constant PAIRING_POINTS_SIZE = 8; + +uint256 constant FIELD_ELEMENT_SIZE = 0x20; +uint256 constant GROUP_ELEMENT_SIZE = 0x40; + +// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) +uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; + +// ENUM FOR WIRES +enum WIRE { + Q_M, + Q_C, + Q_L, + Q_R, + Q_O, + Q_4, + Q_LOOKUP, + Q_ARITH, + Q_RANGE, + Q_ELLIPTIC, + Q_MEMORY, + Q_NNF, + Q_POSEIDON2_EXTERNAL, + Q_POSEIDON2_INTERNAL, + SIGMA_1, + SIGMA_2, + SIGMA_3, + SIGMA_4, + ID_1, + ID_2, + ID_3, + ID_4, + TABLE_1, + TABLE_2, + TABLE_3, + TABLE_4, + LAGRANGE_FIRST, + LAGRANGE_LAST, + W_L, + W_R, + W_O, + W_4, + Z_PERM, + LOOKUP_INVERSES, + LOOKUP_READ_COUNTS, + LOOKUP_READ_TAGS, + W_L_SHIFT, + W_R_SHIFT, + W_O_SHIFT, + W_4_SHIFT, + Z_PERM_SHIFT +} + +library Honk { + struct G1Point { + uint256 x; + uint256 y; + } + + struct VerificationKey { + // Misc Params + uint256 circuitSize; + uint256 logCircuitSize; + uint256 publicInputsSize; + // Selectors + G1Point qm; + G1Point qc; + G1Point ql; + G1Point qr; + G1Point qo; + G1Point q4; + G1Point qLookup; // Lookup + G1Point qArith; // Arithmetic widget + G1Point qDeltaRange; // Delta Range sort + G1Point qMemory; // Memory + G1Point qNnf; // Non-native Field + G1Point qElliptic; // Auxillary + G1Point qPoseidon2External; + G1Point qPoseidon2Internal; + // Copy constraints + G1Point s1; + G1Point s2; + G1Point s3; + G1Point s4; + // Copy identity + G1Point id1; + G1Point id2; + G1Point id3; + G1Point id4; + // Precomputed lookup table + G1Point t1; + G1Point t2; + G1Point t3; + G1Point t4; + // Fixed first and last + G1Point lagrangeFirst; + G1Point lagrangeLast; + } + + struct RelationParameters { + // challenges + Fr eta; + Fr beta; + Fr gamma; + // derived + Fr publicInputsDelta; + } + + struct Proof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // Free wires + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Lookup helpers - Permutations + G1Point zPerm; + // Lookup helpers - logup + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Sumcheck + Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + G1Point shplonkQ; + G1Point kzgQuotient; + } + + /// forge-lint: disable-next-item(pascal-case-struct) + struct ZKProof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) + G1Point geminiMaskingPoly; + // Commitments to wire polynomials + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Commitments to logup witness polynomials + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Commitment to grand permutation polynomial + G1Point zPerm; + G1Point[3] libraCommitments; + // Sumcheck + Fr libraSum; + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr libraEvaluation; + Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + Fr[4] libraPolyEvals; + G1Point shplonkQ; + G1Point kzgQuotient; + } +} + +// ZKTranscript library to generate fiat shamir challenges, the ZK transcript only differest +/// forge-lint: disable-next-item(pascal-case-struct) +struct ZKTranscript { + // Oink + Honk.RelationParameters relationParameters; + Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] + Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; + // Sumcheck + Fr libraChallenge; + Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; + // Shplemini + Fr rho; + Fr geminiR; + Fr shplonkNu; + Fr shplonkZ; + // Derived + Fr publicInputsDelta; +} + +library ZKTranscriptLib { + function generateTranscript( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + uint256 logN + ) external pure returns (ZKTranscript memory t) { + Fr previousChallenge; + (t.relationParameters, previousChallenge) = + generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); + + (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); + + (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); + (t.libraChallenge, previousChallenge) = generateLibraChallenge(previousChallenge, proof); + (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); + + (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); + + (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); + + (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); + + (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); + return t; + } + + function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { + uint256 challengeU256 = uint256(Fr.unwrap(challenge)); + // Split into two equal 127-bit chunks (254/2) + uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits + uint256 hi = challengeU256 >> 127; + first = FrLib.from(lo); + second = FrLib.from(hi); + } + + function generateRelationParametersChallenges( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + Fr previousChallenge + ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { + (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); + + (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); + } + + function generateEtaChallenge( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize + ) internal pure returns (Fr eta, Fr previousChallenge) { + // Size: 1 (vkHash) + publicInputsSize + 8 (geminiMask(2) + 3 wires(6)) + bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 8); + round0[0] = bytes32(vkHash); + + for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { + require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); + round0[1 + i] = publicInputs[i]; + } + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); + } + + // For ZK flavors: hash the gemini masking poly commitment (sent right after public inputs) + round0[1 + publicInputsSize] = bytes32(proof.geminiMaskingPoly.x); + round0[1 + publicInputsSize + 1] = bytes32(proof.geminiMaskingPoly.y); + + // Create the first challenge + // Note: w4 is added to the challenge later on + round0[1 + publicInputsSize + 2] = bytes32(proof.w1.x); + round0[1 + publicInputsSize + 3] = bytes32(proof.w1.y); + round0[1 + publicInputsSize + 4] = bytes32(proof.w2.x); + round0[1 + publicInputsSize + 5] = bytes32(proof.w2.y); + round0[1 + publicInputsSize + 6] = bytes32(proof.w3.x); + round0[1 + publicInputsSize + 7] = bytes32(proof.w3.y); + + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); + (eta,) = splitChallenge(previousChallenge); + } + + function generateBetaGammaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) + { + bytes32[7] memory round1; + round1[0] = FrLib.toBytes32(previousChallenge); + round1[1] = bytes32(proof.lookupReadCounts.x); + round1[2] = bytes32(proof.lookupReadCounts.y); + round1[3] = bytes32(proof.lookupReadTags.x); + round1[4] = bytes32(proof.lookupReadTags.y); + round1[5] = bytes32(proof.w4.x); + round1[6] = bytes32(proof.w4.y); + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); + (beta, gamma) = splitChallenge(nextPreviousChallenge); + } + + // Alpha challenges non-linearise the gate contributions + function generateAlphaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) + { + // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup + uint256[5] memory alpha0; + alpha0[0] = Fr.unwrap(previousChallenge); + alpha0[1] = proof.lookupInverses.x; + alpha0[2] = proof.lookupInverses.y; + alpha0[3] = proof.zPerm.x; + alpha0[4] = proof.zPerm.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); + Fr alpha; + (alpha,) = splitChallenge(nextPreviousChallenge); + + // Compute powers of alpha for batching subrelations + alphas[0] = alpha; + for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { + alphas[i] = alphas[i - 1] * alpha; + } + } + + function generateGateChallenges(Fr previousChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) + { + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); + (gateChallenges[0],) = splitChallenge(previousChallenge); + for (uint256 i = 1; i < logN; i++) { + gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; + } + nextPreviousChallenge = previousChallenge; + } + + function generateLibraChallenge(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr libraChallenge, Fr nextPreviousChallenge) + { + // 2 comm, 1 sum, 1 challenge + uint256[4] memory challengeData; + challengeData[0] = Fr.unwrap(previousChallenge); + challengeData[1] = proof.libraCommitments[0].x; + challengeData[2] = proof.libraCommitments[0].y; + challengeData[3] = Fr.unwrap(proof.libraSum); + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(challengeData))) % P); + (libraChallenge,) = splitChallenge(nextPreviousChallenge); + } + + function generateSumcheckChallenges(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) + { + for (uint256 i = 0; i < logN; i++) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; + univariateChal[0] = prevChallenge; + + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; + } + prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); + + (sumcheckChallenges[i],) = splitChallenge(prevChallenge); + } + nextPreviousChallenge = prevChallenge; + } + + // We add Libra claimed eval + 2 libra commitments (grand_sum, quotient) + function generateRhoChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr rho, Fr nextPreviousChallenge) + { + uint256[NUMBER_OF_ENTITIES_ZK + 6] memory rhoChallengeElements; + rhoChallengeElements[0] = Fr.unwrap(prevChallenge); + uint256 i; + for (i = 1; i <= NUMBER_OF_ENTITIES_ZK; i++) { + rhoChallengeElements[i] = Fr.unwrap(proof.sumcheckEvaluations[i - 1]); + } + rhoChallengeElements[i] = Fr.unwrap(proof.libraEvaluation); + i += 1; + rhoChallengeElements[i] = proof.libraCommitments[1].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[1].y; + i += 2; + rhoChallengeElements[i] = proof.libraCommitments[2].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[2].y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); + (rho,) = splitChallenge(nextPreviousChallenge); + } + + function generateGeminiRChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr geminiR, Fr nextPreviousChallenge) + { + uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); + gR[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 0; i < logN - 1; i++) { + gR[1 + i * 2] = proof.geminiFoldComms[i].x; + gR[2 + i * 2] = proof.geminiFoldComms[i].y; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); + + (geminiR,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkNuChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr shplonkNu, Fr nextPreviousChallenge) + { + uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1 + 4); + shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 1; i <= logN; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.geminiAEvaluations[i - 1]); + } + + uint256 libraIdx = 0; + for (uint256 i = logN + 1; i <= logN + 4; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.libraPolyEvals[libraIdx]); + libraIdx++; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); + (shplonkNu,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkZChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr shplonkZ, Fr nextPreviousChallenge) + { + uint256[3] memory shplonkZChallengeElements; + shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); + + shplonkZChallengeElements[1] = proof.shplonkQ.x; + shplonkZChallengeElements[2] = proof.shplonkQ.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); + (shplonkZ,) = splitChallenge(nextPreviousChallenge); + } + + function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.ZKProof memory p) { + uint256 boundary = 0x0; + + // Pairing point object + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); + // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 + require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); + p.pairingPointObject[i] = FrLib.from(limb); + boundary += FIELD_ELEMENT_SIZE; + } + + // Gemini masking polynomial commitment (sent first in ZK flavors, right after pairing points) + p.geminiMaskingPoly = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Commitments + p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Lookup / Permutation Helper Commitments + p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[0] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + p.libraSum = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + // Sumcheck univariates + for (uint256 i = 0; i < logN; i++) { + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + } + + // Sumcheck evaluations (includes gemini_masking_poly eval at index 0 for ZK flavors) + for (uint256 i = 0; i < NUMBER_OF_ENTITIES_ZK; i++) { + p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + p.libraEvaluation = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + + p.libraCommitments[1] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[2] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Gemini + // Read gemini fold univariates + for (uint256 i = 0; i < logN - 1; i++) { + p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + } + + // Read gemini a evaluations + for (uint256 i = 0; i < logN; i++) { + p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + for (uint256 i = 0; i < 4; i++) { + p.libraPolyEvals[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + // Shplonk + p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + // KZG + p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + } +} + +library RelationsLib { + struct EllipticParams { + // Points + Fr x_1; + Fr y_1; + Fr x_2; + Fr y_2; + Fr y_3; + Fr x_3; + // push accumulators into memory + Fr x_double_identity; + } + + // Parameters used within the Memory Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct MemParams { + Fr memory_record_check; + Fr partial_record_check; + Fr next_gate_access_type; + Fr record_delta; + Fr index_delta; + Fr adjacent_values_match_if_adjacent_indices_match; + Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; + Fr access_check; + Fr next_gate_access_type_is_boolean; + Fr ROM_consistency_check_identity; + Fr RAM_consistency_check_identity; + Fr timestamp_delta; + Fr RAM_timestamp_check_identity; + Fr memory_identity; + Fr index_is_monotonically_increasing; + } + + // Parameters used within the Non-Native Field Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct NnfParams { + Fr limb_subproduct; + Fr non_native_field_gate_1; + Fr non_native_field_gate_2; + Fr non_native_field_gate_3; + Fr limb_accumulator_1; + Fr limb_accumulator_2; + Fr nnf_identity; + } + + struct PoseidonExternalParams { + Fr s1; + Fr s2; + Fr s3; + Fr s4; + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr t0; + Fr t1; + Fr t2; + Fr t3; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr q_pos_by_scaling; + } + + struct PoseidonInternalParams { + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr u_sum; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr s1; + Fr q_pos_by_scaling; + } + + Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) + uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; + + // Constants for the Non-native Field relation + Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); + Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); + + function accumulateRelationEvaluations( + Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, + Fr powPartialEval + ) internal pure returns (Fr accumulator) { + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; + + // Accumulate all relations in Ultra Honk - each with varying number of subrelations + accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); + + // batch the subrelations with the precomputed alpha powers to obtain the full honk relation + accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); + } + + /** + * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids + * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code + * editors, and thus is noisy. + */ + function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { + return p[uint256(_wire)]; + } + + /** + * Ultra Arithmetic Relation + * + */ + function accumulateArithmeticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + // Relation 0 + Fr q_arith = wire(p, WIRE.Q_ARITH); + { + Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); + + Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; + accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) + + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); + accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); + accum = accum * q_arith; + accum = accum * domainSep; + evals[0] = accum; + } + + // Relation 1 + { + Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); + accum = accum * (q_arith - Fr.wrap(2)); + accum = accum * (q_arith - ONE); + accum = accum * q_arith; + accum = accum * domainSep; + evals[1] = accum; + } + } + + function accumulatePermutationRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr grand_product_numerator; + Fr grand_product_denominator; + + { + Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; + num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); + + grand_product_numerator = num; + } + { + Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; + den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); + + grand_product_denominator = den; + } + + // Contribution 2 + { + Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; + + acc = acc + - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) + * grand_product_denominator); + acc = acc * domainSep; + evals[2] = acc; + } + + // Contribution 3 + { + Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; + evals[3] = acc; + } + } + + function accumulateLogDerivativeLookupRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr table_term; + Fr lookup_term; + + // Calculate the write term (the table accumulation) + // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) + + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); + } + + // Calculate the read term + // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); + Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); + Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); + + lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) + + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); + } + + Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; + Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; + + Fr inverse_exists_xor = + wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) + - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); + + // Inverse calculated correctly relation + Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; + accumulatorNone = accumulatorNone * domainSep; + + // Inverse + Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; + + Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); + + Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; + + evals[4] = accumulatorNone; + evals[5] = accumulatorOne; + evals[6] = read_tag_boolean_relation * domainSep; + } + + function accumulateDeltaRangeRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr minus_one = ZERO - ONE; + Fr minus_two = ZERO - Fr.wrap(2); + Fr minus_three = ZERO - Fr.wrap(3); + + // Compute wire differences + Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); + Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); + Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); + Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); + + // Contribution 6 + { + Fr acc = delta_1; + acc = acc * (delta_1 + minus_one); + acc = acc * (delta_1 + minus_two); + acc = acc * (delta_1 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[7] = acc; + } + + // Contribution 7 + { + Fr acc = delta_2; + acc = acc * (delta_2 + minus_one); + acc = acc * (delta_2 + minus_two); + acc = acc * (delta_2 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[8] = acc; + } + + // Contribution 8 + { + Fr acc = delta_3; + acc = acc * (delta_3 + minus_one); + acc = acc * (delta_3 + minus_two); + acc = acc * (delta_3 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[9] = acc; + } + + // Contribution 9 + { + Fr acc = delta_4; + acc = acc * (delta_4 + minus_one); + acc = acc * (delta_4 + minus_two); + acc = acc * (delta_4 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[10] = acc; + } + } + + function accumulateEllipticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + EllipticParams memory ep; + ep.x_1 = wire(p, WIRE.W_R); + ep.y_1 = wire(p, WIRE.W_O); + + ep.x_2 = wire(p, WIRE.W_L_SHIFT); + ep.y_2 = wire(p, WIRE.W_4_SHIFT); + ep.y_3 = wire(p, WIRE.W_O_SHIFT); + ep.x_3 = wire(p, WIRE.W_R_SHIFT); + + Fr q_sign = wire(p, WIRE.Q_L); + Fr q_is_double = wire(p, WIRE.Q_M); + + // Contribution 10 point addition, x-coordinate check + // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 + Fr x_diff = (ep.x_2 - ep.x_1); + Fr y1_sqr = (ep.y_1 * ep.y_1); + { + // Move to top + Fr partialEval = domainSep; + + Fr y2_sqr = (ep.y_2 * ep.y_2); + Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; + Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); + x_add_identity = x_add_identity * x_diff * x_diff; + x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; + + evals[11] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 11 point addition, x-coordinate check + // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 + { + Fr y1_plus_y3 = ep.y_1 + ep.y_3; + Fr y_diff = ep.y_2 * q_sign - ep.y_1; + Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; + evals[12] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 10 point doubling, x-coordinate check + // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 + // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 + { + Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; + Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; + y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; + Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); + + // NOTE: pushed into memory (stack >:'( ) + ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; + + Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + evals[11] = evals[11] + acc; + } + + // Contribution 11 point doubling, y-coordinate check + // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 + { + Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; + Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); + evals[12] = evals[12] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + } + } + + function accumulateMemoryRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + MemParams memory ap; + + // Compute eta powers locally + Fr eta_two = rp.eta * rp.eta; + Fr eta_three = eta_two * rp.eta; + + /** + * MEMORY + * + * A RAM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) + * * v: `value` of memory cell being accessed + * * a: `access` type of record. read: 0 = read, 1 = write + * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three + * + * A ROM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three + * + * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + + * selectors, depending on whether the gate is a RAM read/write or a ROM read + * + * | gate type | i | v2/t | v | a | r | + * | --------- | -- | ----- | -- | -- | -- | + * | ROM | w1 | w2 | w3 | -- | w4 | + * | RAM | w1 | w2 | w3 | qc | w4 | + * + * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on + * `w2` to fix its value) + * + * + */ + + /** + * Memory Record Check + * Partial degree: 1 + * Total degree: 4 + * + * A ROM/ROM access gate can be evaluated with the identity: + * + * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 + * + * For ROM gates, qc = 0 + */ + ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); + ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); + ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 + ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); + + /** + * Contribution 13 & 14 + * ROM Consistency Check + * Partial degree: 1 + * Total degree: 4 + * + * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of + * records that are sorted. + * + * We apply the following checks for the sorted records: + * + * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 + * 2. index values for adjacent records are monotonically increasing + * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} + * + */ + ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); + ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); + + ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 + + ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 + + evals[14] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + evals[15] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + + ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 + + /** + * Contributions 15,16,17 + * RAM Consistency Check + * + * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` + * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. + * This is validated by requiring `access` to be boolean + * + * For two adjacent entries in the sorted list if _both_ + * A) index values match + * B) adjacent access value is 0 (i.e. next gate is a READ) + * then + * C) both values must match. + * The gate boolean check is + * (A && B) => C === !(A && B) || C === !A || !B || C + * + * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized + * with a WRITE operation. + */ + Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 + ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 + + // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta + // deg 1 or 4 + ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); + ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; + + Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); + ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = + (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 + + // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the + // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't + // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access + // type is correct, to cover this edge case + // deg 2 or 4 + ap.next_gate_access_type_is_boolean = + ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; + + // Putting it all together... + evals[16] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation + * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 + evals[17] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 + evals[18] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 6 + + ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 + + /** + * RAM Timestamp Consistency Check + * + * | w1 | w2 | w3 | w4 | + * | index | timestamp | timestamp_check | -- | + * + * Let delta_index = index_{i + 1} - index_{i} + * + * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i + * Else timestamp_check = 0 + */ + ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); + ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg 3 + + /** + * Complete Contribution 12 + * The complete RAM/ROM memory identity + * Partial degree: + */ + ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 + ap.memory_identity = + ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg 4 + ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 or 6 + ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 + + // (deg 3 or 9) + (deg 4) + (deg 3) + ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 + evals[13] = ap.memory_identity; + } + + function accumulateNnfRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + NnfParams memory ap; + + /** + * Contribution 12 + * Non native field arithmetic gate 2 + * deg 4 + * + * _ _ + * / _ _ _ 14 \ + * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | + * \_ _/ + * + * + */ + ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); + ap.non_native_field_gate_2 = + (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); + + ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; + ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); + ap.non_native_field_gate_1 = ap.limb_subproduct; + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); + + ap.non_native_field_gate_3 = ap.limb_subproduct; + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); + + Fr non_native_field_identity = + ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; + non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); + + // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm + // deg 2 + ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); + ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); + + // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm + // deg 2 + ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); + ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); + + Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; + limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 + + ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; + ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); + evals[19] = ap.nnf_identity; + } + + function accumulatePoseidonExternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonExternalParams memory ep; + + ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); + ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); + ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); + + ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; + ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; + ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; + ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; + // matrix mul v = M_E * u with 14 additions + ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 + ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 + ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 + // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 + ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 + // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 + ep.v4 = ep.t1 + ep.t1; + ep.v4 = ep.v4 + ep.v4 + ep.t3; + // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 + ep.v2 = ep.t0 + ep.t0; + ep.v2 = ep.v2 + ep.v2 + ep.t2; + // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 + ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 + ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 + + ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; + evals[20] = evals[20] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); + + evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); + + evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); + + evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + function accumulatePoseidonInternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonInternalParams memory ip; + + Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ + FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), + FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), + FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), + FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) + ]; + + // add round constants + ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + + // apply s-box round + ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; + ip.u2 = wire(p, WIRE.W_R); + ip.u3 = wire(p, WIRE.W_O); + ip.u4 = wire(p, WIRE.W_4); + + // matrix mul with v = M_I * u 4 muls and 7 additions + ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; + + ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; + + ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; + evals[24] = evals[24] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); + + ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; + evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); + + ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; + evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); + + ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; + evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + // Batch subrelation evaluations using precomputed powers of alpha + // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array + function scaleAndBatchSubrelations( + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges + ) internal pure returns (Fr accumulator) { + accumulator = evaluations[0]; + + for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { + accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; + } + } +} + +library CommitmentSchemeLib { + using FrLib for Fr; + + // Avoid stack too deep + struct ShpleminiIntermediates { + Fr unshiftedScalar; + Fr shiftedScalar; + Fr unshiftedScalarNeg; + Fr shiftedScalarNeg; + // Scalar to be multiplied by [1]₁ + Fr constantTermAccumulator; + // Accumulator for powers of rho + Fr batchingChallenge; + // Linear combination of multilinear (sumcheck) evaluations and powers of rho + Fr batchedEvaluation; + Fr[4] denominators; + Fr[4] batchingScalars; + // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr posInvertedDenominator; + // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr negInvertedDenominator; + // ν^{2i} * 1/(z - r^{2^i}) + Fr scalingFactorPos; + // ν^{2i+1} * 1/(z + r^{2^i}) + Fr scalingFactorNeg; + // Fold_i(r^{2^i}) reconstructed by Verifier + Fr[] foldPosEvaluations; + } + + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 + function computeFoldPosEvaluations( + Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, + Fr batchedEvalAccumulator, + Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, + Fr[] memory geminiEvalChallengePowers, + uint256 logSize + ) internal view returns (Fr[] memory) { + Fr[] memory foldPosEvaluations = new Fr[](logSize); + for (uint256 i = logSize; i > 0; --i) { + Fr challengePower = geminiEvalChallengePowers[i - 1]; + Fr u = sumcheckUChallenges[i - 1]; + + Fr batchedEvalRoundAcc = ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] + * (challengePower * (ONE - u) - u)); + // Divide by the denominator + batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); + + batchedEvalAccumulator = batchedEvalRoundAcc; + foldPosEvaluations[i - 1] = batchedEvalRoundAcc; + } + return foldPosEvaluations; + } + + function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { + Fr[] memory squares = new Fr[](logN); + squares[0] = r; + for (uint256 i = 1; i < logN; ++i) { + squares[i] = squares[i - 1].sqr(); + } + return squares; + } +} + +uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // EC group order. F_q + +// Fr utility + +function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { + scalar = FrLib.fromBytes32(bytes32(proofSection)); +} + +// EC Point utilities +function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { + uint256 x = uint256(bytes32(proofSection[0x00:0x20])); + uint256 y = uint256(bytes32(proofSection[0x20:0x40])); + require(x < Q && y < Q, Errors.ValueGeGroupOrder()); + + // Reject the point at infinity (0,0). EVM precompiles silently treat (0,0) + // as the identity element, which could zero out commitments. + // On-curve validation (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles + // per EIP-196, so we only need to catch this special case here. + require((x | y) != 0, Errors.PointAtInfinity()); + + point = Honk.G1Point({x: x, y: y}); +} + +function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { + // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. + if (point.y != 0) { + point.y = Q - point.y; + } + return point; +} + +/** + * Convert the pairing points to G1 points. + * + * The pairing points are serialised as an array of 2 limbs representing two points + * (P0 and P1, used for lhs and rhs of pairing operation). + * + * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. + * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] + * + * @param pairingPoints The pairing points to convert. + * @return lhs P0 point + * @return rhs P1 point + */ +function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) + pure + returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) +{ + // P0 (lhs): x = lo | (hi << 136) + uint256 lhsX = Fr.unwrap(pairingPoints[0]); + lhsX |= Fr.unwrap(pairingPoints[1]) << 136; + + uint256 lhsY = Fr.unwrap(pairingPoints[2]); + lhsY |= Fr.unwrap(pairingPoints[3]) << 136; + + // P1 (rhs): x = lo | (hi << 136) + uint256 rhsX = Fr.unwrap(pairingPoints[4]); + rhsX |= Fr.unwrap(pairingPoints[5]) << 136; + + uint256 rhsY = Fr.unwrap(pairingPoints[6]); + rhsY |= Fr.unwrap(pairingPoints[7]) << 136; + + // Reconstructed coordinates must be < Q to prevent malleability. + // Without this, two different limb encodings could map to the same curve point + // (via mulmod reduction in on-curve checks) but produce different transcript hashes. + require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); + + lhs.x = lhsX; + lhs.y = lhsY; + rhs.x = rhsX; + rhs.y = rhsY; +} + +/** + * Hash the pairing inputs from the present verification context with those extracted from the public inputs. + * + * @param proofPairingPoints Pairing points from the proof - (public inputs). + * @param accLhs Accumulator point for the left side - result of shplemini. + * @param accRhs Accumulator point for the right side - result of shplemini. + * @return recursionSeparator The recursion separator - generated from hashing the above. + */ +function generateRecursionSeparator( + Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, + Honk.G1Point memory accLhs, + Honk.G1Point memory accRhs +) pure returns (Fr recursionSeparator) { + // hash the proof aggregated X + // hash the proof aggregated Y + // hash the accum X + // hash the accum Y + + (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); + + uint256[8] memory recursionSeparatorElements; + + // Proof points + recursionSeparatorElements[0] = proofLhs.x; + recursionSeparatorElements[1] = proofLhs.y; + recursionSeparatorElements[2] = proofRhs.x; + recursionSeparatorElements[3] = proofRhs.y; + + // Accumulator points + recursionSeparatorElements[4] = accLhs.x; + recursionSeparatorElements[5] = accLhs.y; + recursionSeparatorElements[6] = accRhs.x; + recursionSeparatorElements[7] = accRhs.y; + + recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); +} + +/** + * G1 Mul with Separator + * Using the ecAdd and ecMul precompiles + * + * @param basePoint The point to multiply. + * @param other The other point to add. + * @param recursionSeperator The separator to use for the multiplication. + * @return `(recursionSeperator * basePoint) + other`. + */ +function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) + view + returns (Honk.G1Point memory) +{ + Honk.G1Point memory result; + + result = ecMul(recursionSeperator, basePoint); + result = ecAdd(result, other); + + return result; +} + +/** + * G1 Mul + * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. + * + * @param value The value to multiply the point by. + * @param point The point to multiply. + * @return result The result of the multiplication. + */ +function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write the point into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | point.x + // free + 0x20| point.y + mstore(free, mload(point)) + mstore(add(free, 0x20), mload(add(point, 0x20))) + // Write the scalar into memory (one 32 byte word) + // Memory layout: + // Address | value + // free + 0x40| value + mstore(add(free, 0x40), value) + + // Call the ecMul precompile, it takes in the following + // [point.x, point.y, scalar], and returns the result back into the free memory location. + let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) + if iszero(success) { + revert(0, 0) + } + // Copy the result of the multiplication back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x60)) + } + + return result; +} + +/** + * G1 Add + * Takes two G1 points and uses the ecAdd precompile to return the result. + * + * @param lhs The left hand side of the addition. + * @param rhs The right hand side of the addition. + * @return result The result of the addition. + */ +function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write lhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | lhs.x + // free + 0x20| lhs.y + mstore(free, mload(lhs)) + mstore(add(free, 0x20), mload(add(lhs, 0x20))) + + // Write rhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free + 0x40| rhs.x + // free + 0x60| rhs.y + mstore(add(free, 0x40), mload(rhs)) + mstore(add(free, 0x60), mload(add(rhs, 0x20))) + + // Call the ecAdd precompile, it takes in the following + // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. + let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) + if iszero(success) { revert(0, 0) } + + // Copy the result of the addition back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x80)) + } + + return result; +} + +function rejectPointAtInfinity(Honk.G1Point memory point) pure { + require((point.x | point.y) != 0, Errors.PointAtInfinity()); +} + +/** + * Check if pairing point limbs are all zero (default/infinity). + * Default pairing points indicate no recursive verification occurred. + */ +function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { + uint256 acc = 0; + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + acc |= Fr.unwrap(pairingPoints[i]); + } + return acc == 0; +} + +function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { + bytes memory input = abi.encodePacked( + rhs.x, + rhs.y, + // Fixed G2 point + uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), + uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), + uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), + uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), + lhs.x, + lhs.y, + // G2 point from VK + uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), + uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), + uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), + uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) + ); + + (bool success, bytes memory result) = address(0x08).staticcall(input); + decodedResult = success && abi.decode(result, (bool)); +} + +abstract contract BaseZKHonkVerifier is IVerifier { + using FrLib for Fr; + + struct PairingInputs { + Honk.G1Point P_0; + Honk.G1Point P_1; + } + + struct SmallSubgroupIpaIntermediates { + Fr[SUBGROUP_SIZE] challengePolyLagrange; + Fr challengePolyEval; + Fr lagrangeFirst; + Fr lagrangeLast; + Fr rootPower; + Fr[SUBGROUP_SIZE] denominators; // this has to disappear + Fr diff; + } + + // Constants for proof length calculation (matching UltraKeccakZKFlavor) + uint256 internal constant NUM_WITNESS_ENTITIES = 8 + NUM_MASKING_POLYNOMIALS; + uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points + uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements + uint256 internal constant NUM_LIBRA_EVALUATIONS = 4; // libra evaluations + + uint256 internal constant LIBRA_COMMITMENTS = 3; + uint256 internal constant LIBRA_EVALUATIONS = 4; + uint256 internal constant LIBRA_UNIVARIATES_LENGTH = 9; + + uint256 internal constant SHIFTED_COMMITMENTS_START = 30; + uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; + + uint256 internal immutable $N; + uint256 internal immutable $LOG_N; + uint256 internal immutable $VK_HASH; + uint256 internal immutable $NUM_PUBLIC_INPUTS; + uint256 internal immutable $MSMSize; + + constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { + $N = _N; + $LOG_N = _logN; + $VK_HASH = _vkHash; + $NUM_PUBLIC_INPUTS = _numPublicInputs; + $MSMSize = NUMBER_UNSHIFTED_ZK + _logN + LIBRA_COMMITMENTS + 2; + } + + function verify(bytes calldata proof, bytes32[] calldata publicInputs) + public + view + override + returns (bool verified) + { + // Calculate expected proof size based on $LOG_N + uint256 expectedProofSize = calculateProofSize($LOG_N); + + // Check the received proof is the expected size where each field element is 32 bytes + require( + proof.length == expectedProofSize, Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize) + ); + + Honk.VerificationKey memory vk = loadVerificationKey(); + Honk.ZKProof memory p = ZKTranscriptLib.loadProof(proof, $LOG_N); + + require(publicInputs.length == vk.publicInputsSize - PAIRING_POINTS_SIZE, Errors.PublicInputsLengthWrong()); + + // Generate the fiat shamir challenges for the whole protocol + ZKTranscript memory t = + ZKTranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); + + // Derive public input delta + t.relationParameters.publicInputsDelta = computePublicInputDelta( + publicInputs, + p.pairingPointObject, + t.relationParameters.beta, + t.relationParameters.gamma, /*pubInputsOffset=*/ + 1 + ); + + // Sumcheck + require(verifySumcheck(p, t), Errors.SumcheckFailed()); + require(verifyShplemini(p, vk, t), Errors.ShpleminiFailed()); + + verified = true; + } + + function computePublicInputDelta( + bytes32[] memory publicInputs, + Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, + Fr beta, + Fr gamma, + uint256 offset + ) internal view returns (Fr publicInputDelta) { + Fr numerator = Fr.wrap(1); + Fr denominator = Fr.wrap(1); + + Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); + Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); + + { + for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { + Fr pubInput = FrLib.fromBytes32(publicInputs[i]); + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + Fr pubInput = pairingPointObject[i]; + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + } + + // Fr delta = numerator / denominator; // TOOO: batch invert later? + publicInputDelta = FrLib.div(numerator, denominator); + } + + function verifySumcheck(Honk.ZKProof memory proof, ZKTranscript memory tp) internal view returns (bool verified) { + Fr roundTargetSum = tp.libraChallenge * proof.libraSum; // default 0 + Fr powPartialEvaluation = Fr.wrap(1); + + // We perform sumcheck reductions over log n rounds ( the multivariate degree ) + for (uint256 round; round < $LOG_N; ++round) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; + Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; + require(totalSum == roundTargetSum, Errors.SumcheckFailed()); + + Fr roundChallenge = tp.sumCheckUChallenges[round]; + + // Update the round target for the next rounf + roundTargetSum = computeNextTargetSum(roundUnivariate, roundChallenge); + powPartialEvaluation = + powPartialEvaluation * (Fr.wrap(1) + roundChallenge * (tp.gateChallenges[round] - Fr.wrap(1))); + } + + // Last round + // For ZK flavors: sumcheckEvaluations has 42 elements + // Index 0 is gemini_masking_poly, indices 1-41 are the regular entities used in relations + Fr[NUMBER_OF_ENTITIES] memory relationsEvaluations; + for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { + relationsEvaluations[i] = proof.sumcheckEvaluations[i + NUM_MASKING_POLYNOMIALS]; // Skip gemini_masking_poly at index 0 + } + Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( + relationsEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation + ); + + Fr evaluation = Fr.wrap(1); + for (uint256 i = 2; i < $LOG_N; i++) { + evaluation = evaluation * tp.sumCheckUChallenges[i]; + } + + grandHonkRelationSum = + grandHonkRelationSum * (Fr.wrap(1) - evaluation) + proof.libraEvaluation * tp.libraChallenge; + verified = (grandHonkRelationSum == roundTargetSum); + } + + // Return the new target sum for the next sumcheck round + function computeNextTargetSum(Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) + internal + view + returns (Fr targetSum) + { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000240), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80) + ]; + + // To compute the next target sum, we evaluate the given univariate at a point u (challenge). + + // Performing Barycentric evaluations + // Compute B(x) + Fr numeratorValue = Fr.wrap(1); + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); + } + + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + denominatorInverses[i] = FrLib.invert(BARYCENTRIC_LAGRANGE_DENOMINATORS[i] * (roundChallenge - Fr.wrap(i))); + } + + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + targetSum = targetSum + roundUnivariates[i] * denominatorInverses[i]; + } + + // Scale the sum by the value of B(x) + targetSum = targetSum * numeratorValue; + } + + function verifyShplemini(Honk.ZKProof memory proof, Honk.VerificationKey memory vk, ZKTranscript memory tp) + internal + view + returns (bool verified) + { + CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack + + // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size + Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); + // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings + Fr[] memory scalars = new Fr[]($MSMSize); + Honk.G1Point[] memory commitments = new Honk.G1Point[]($MSMSize); + + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); + + mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); + mem.shiftedScalar = + tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); + + scalars[0] = Fr.wrap(1); + commitments[0] = proof.shplonkQ; + + /* Batch multivariate opening claims, shifted and unshifted + * The vector of scalars is populated as follows: + * \f[ + * \left( + * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) + * \right) + * \f] + * + * The following vector is concatenated to the vector of commitments: + * \f[ + * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} + * \f] + * + * Simultaneously, the evaluation of the multilinear polynomial + * \f[ + * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} + * \f] + * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. + * + * This approach minimizes the number of iterations over the commitments to multilinear polynomials + * and eliminates the need to store the powers of \f$ \rho \f$. + */ + // For ZK flavors: evaluations array is [gemini_masking_poly, qm, qc, ql, qr, ...] + // Start batching challenge at 1, not rho, to match non-ZK pattern + mem.batchingChallenge = Fr.wrap(1); + mem.batchedEvaluation = Fr.wrap(0); + + mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); + mem.shiftedScalarNeg = mem.shiftedScalar.neg(); + + // Process all NUMBER_UNSHIFTED_ZK evaluations (includes gemini_masking_poly at index 0) + for (uint256 i = 1; i <= NUMBER_UNSHIFTED_ZK; ++i) { + scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; + mem.batchedEvaluation = mem.batchedEvaluation + + (proof.sumcheckEvaluations[i - NUM_MASKING_POLYNOMIALS] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + // g commitments are accumulated at r + // For each of the to be shifted commitments perform the shift in place by + // adding to the unshifted value. + // We do so, as the values are to be used in batchMul later, and as + // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. + // Applied to w1, w2, w3, w4 and zPerm + for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { + uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; + uint256 evaluationOff = i + NUMBER_UNSHIFTED_ZK; + + scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); + mem.batchedEvaluation = + mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + + commitments[1] = proof.geminiMaskingPoly; + + commitments[2] = vk.qm; + commitments[3] = vk.qc; + commitments[4] = vk.ql; + commitments[5] = vk.qr; + commitments[6] = vk.qo; + commitments[7] = vk.q4; + commitments[8] = vk.qLookup; + commitments[9] = vk.qArith; + commitments[10] = vk.qDeltaRange; + commitments[11] = vk.qElliptic; + commitments[12] = vk.qMemory; + commitments[13] = vk.qNnf; + commitments[14] = vk.qPoseidon2External; + commitments[15] = vk.qPoseidon2Internal; + commitments[16] = vk.s1; + commitments[17] = vk.s2; + commitments[18] = vk.s3; + commitments[19] = vk.s4; + commitments[20] = vk.id1; + commitments[21] = vk.id2; + commitments[22] = vk.id3; + commitments[23] = vk.id4; + commitments[24] = vk.t1; + commitments[25] = vk.t2; + commitments[26] = vk.t3; + commitments[27] = vk.t4; + commitments[28] = vk.lagrangeFirst; + commitments[29] = vk.lagrangeLast; + + // Accumulate proof points + commitments[30] = proof.w1; + commitments[31] = proof.w2; + commitments[32] = proof.w3; + commitments[33] = proof.w4; + commitments[34] = proof.zPerm; + commitments[35] = proof.lookupInverses; + commitments[36] = proof.lookupReadCounts; + commitments[37] = proof.lookupReadTags; + + /* Batch gemini claims from the prover + * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from + * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars + * + * 1. Moves the vector + * \f[ + * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) + * \f] + * to the 'commitments' vector. + * + * 2. Computes the scalars: + * \f[ + * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} + * \f] + * and places them into the 'scalars' vector. + * + * 3. Accumulates the summands of the constant term: + * \f[ + * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} + * \f] + * and adds them to the 'constant_term_accumulator'. + */ + + // Add contributions from A₀(r) and A₀(-r) to constant_term_accumulator: + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 + Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( + tp.sumCheckUChallenges, + mem.batchedEvaluation, + proof.geminiAEvaluations, + powers_of_evaluation_challenge, + $LOG_N + ); + + mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; + mem.constantTermAccumulator = + mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); + + mem.batchingChallenge = tp.shplonkNu.sqr(); + uint256 boundary = NUMBER_UNSHIFTED_ZK + 1; + + // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; + // Compute scalar multipliers for each fold commitment + for (uint256 i = 0; i < $LOG_N - 1; ++i) { + bool dummy_round = i >= ($LOG_N - 1); + + if (!dummy_round) { + // Update inverted denominators + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); + + // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] + mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; + mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; + scalars[boundary + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); + + // Accumulate the const term contribution given by + // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) + Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; + accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; + mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; + } + // Update the running power of v + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; + + commitments[boundary + i] = proof.geminiFoldComms[i]; + } + + boundary += $LOG_N - 1; + + // Finalize the batch opening claim + mem.denominators[0] = Fr.wrap(1).div(tp.shplonkZ - tp.geminiR); + mem.denominators[1] = Fr.wrap(1).div(tp.shplonkZ - SUBGROUP_GENERATOR * tp.geminiR); + mem.denominators[2] = mem.denominators[0]; + mem.denominators[3] = mem.denominators[0]; + + for (uint256 i = 0; i < LIBRA_EVALUATIONS; i++) { + Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; + mem.batchingScalars[i] = scalingFactor.neg(); + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; + mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i]; + } + scalars[boundary] = mem.batchingScalars[0]; + scalars[boundary + 1] = mem.batchingScalars[1] + mem.batchingScalars[2]; + scalars[boundary + 2] = mem.batchingScalars[3]; + + for (uint256 i = 0; i < LIBRA_COMMITMENTS; i++) { + commitments[boundary++] = proof.libraCommitments[i]; + } + + commitments[boundary] = Honk.G1Point({x: 1, y: 2}); + scalars[boundary++] = mem.constantTermAccumulator; + + require( + checkEvalsConsistency(proof.libraPolyEvals, tp.geminiR, tp.sumCheckUChallenges, proof.libraEvaluation), + Errors.ConsistencyCheckFailed() + ); + + Honk.G1Point memory quotient_commitment = proof.kzgQuotient; + + commitments[boundary] = quotient_commitment; + scalars[boundary] = tp.shplonkZ; // evaluation challenge + + PairingInputs memory pair; + pair.P_0 = batchMul(commitments, scalars); + pair.P_1 = negateInplace(quotient_commitment); + + // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) + if (!arePairingPointsDefault(proof.pairingPointObject)) { + Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, pair.P_0, pair.P_1); + (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = + convertPairingPointsToG1(proof.pairingPointObject); + + // Validate the points from the proof are on the curve + rejectPointAtInfinity(P_0_other); + rejectPointAtInfinity(P_1_other); + + // accumulate with aggregate points in proof + pair.P_0 = mulWithSeperator(pair.P_0, P_0_other, recursionSeparator); + pair.P_1 = mulWithSeperator(pair.P_1, P_1_other, recursionSeparator); + } + + return pairing(pair.P_0, pair.P_1); + } + + function checkEvalsConsistency( + Fr[LIBRA_EVALUATIONS] memory libraPolyEvals, + Fr geminiR, + Fr[CONST_PROOF_SIZE_LOG_N] memory uChallenges, + Fr libraEval + ) internal view returns (bool check) { + Fr one = Fr.wrap(1); + Fr vanishingPolyEval = geminiR.pow(SUBGROUP_SIZE) - one; + require(vanishingPolyEval != Fr.wrap(0), Errors.GeminiChallengeInSubgroup()); + + SmallSubgroupIpaIntermediates memory mem; + mem.challengePolyLagrange[0] = one; + for (uint256 round = 0; round < $LOG_N; round++) { + uint256 currIdx = 1 + LIBRA_UNIVARIATES_LENGTH * round; + mem.challengePolyLagrange[currIdx] = one; + for (uint256 idx = currIdx + 1; idx < currIdx + LIBRA_UNIVARIATES_LENGTH; idx++) { + mem.challengePolyLagrange[idx] = mem.challengePolyLagrange[idx - 1] * uChallenges[round]; + } + } + + mem.rootPower = one; + mem.challengePolyEval = Fr.wrap(0); + for (uint256 idx = 0; idx < SUBGROUP_SIZE; idx++) { + mem.denominators[idx] = mem.rootPower * geminiR - one; + mem.denominators[idx] = mem.denominators[idx].invert(); + mem.challengePolyEval = mem.challengePolyEval + mem.challengePolyLagrange[idx] * mem.denominators[idx]; + mem.rootPower = mem.rootPower * SUBGROUP_GENERATOR_INVERSE; + } + + Fr numerator = vanishingPolyEval * Fr.wrap(SUBGROUP_SIZE).invert(); + mem.challengePolyEval = mem.challengePolyEval * numerator; + mem.lagrangeFirst = mem.denominators[0] * numerator; + mem.lagrangeLast = mem.denominators[SUBGROUP_SIZE - 1] * numerator; + + mem.diff = mem.lagrangeFirst * libraPolyEvals[2]; + + mem.diff = mem.diff + (geminiR - SUBGROUP_GENERATOR_INVERSE) + * (libraPolyEvals[1] - libraPolyEvals[2] - libraPolyEvals[0] * mem.challengePolyEval); + mem.diff = mem.diff + mem.lagrangeLast * (libraPolyEvals[2] - libraEval) - vanishingPolyEval * libraPolyEvals[3]; + + check = mem.diff == Fr.wrap(0); + } + + // This implementation is the same as above with different constants + function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) + internal + view + returns (Honk.G1Point memory result) + { + uint256 limit = $MSMSize; + + // Validate all points are on the curve + for (uint256 i = 0; i < limit; ++i) { + rejectPointAtInfinity(base[i]); + } + + bool success = true; + assembly { + let free := mload(0x40) + + let count := 0x01 + for {} lt(count, add(limit, 1)) { count := add(count, 1) } { + // Get loop offsets + let base_base := add(base, mul(count, 0x20)) + let scalar_base := add(scalars, mul(count, 0x20)) + + mstore(add(free, 0x40), mload(mload(base_base))) + mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) + // Add scalar + mstore(add(free, 0x80), mload(scalar_base)) + + success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) + // accumulator = accumulator + accumulator_2 + success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) + } + + // Return the result + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + } + + require(success, Errors.ShpleminiFailed()); + } + + // Calculate proof size based on log_n (matching UltraKeccakZKFlavor formula) + function calculateProofSize(uint256 logN) internal pure returns (uint256) { + // Witness and Libra commitments + uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments + proofLength += NUM_ELEMENTS_COMM * 3; // Libra concat, grand sum, quotient comms + Gemini masking + + // Sumcheck + proofLength += logN * ZK_BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates + proofLength += NUMBER_OF_ENTITIES_ZK * NUM_ELEMENTS_FR; // sumcheck evaluations + + // Libra and Gemini + proofLength += NUM_ELEMENTS_FR * 2; // Libra sum, claimed eval + proofLength += logN * NUM_ELEMENTS_FR; // Gemini a evaluations + proofLength += NUM_LIBRA_EVALUATIONS * NUM_ELEMENTS_FR; // libra evaluations + + // PCS commitments + proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments + proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments + + // Pairing points + proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs + + return proofLength * 32; + } + + function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); +} + +contract HonkVerifier is BaseZKHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { + function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { + return HonkVerificationKey.loadVerificationKey(); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/WithdrawInsertionVerifier.sol b/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/WithdrawInsertionVerifier.sol new file mode 100644 index 0000000..338ea97 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/WithdrawInsertionVerifier.sol @@ -0,0 +1,2460 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec +pragma solidity >=0.8.21; + +uint256 constant N = 262144; +uint256 constant LOG_N = 18; +uint256 constant NUMBER_OF_PUBLIC_INPUTS = 12; +uint256 constant VK_HASH = 0x29688d0d14c233fc62d404e6aa61ea9888c369229af5160c7fdb4e729d66cda3; +library HonkVerificationKey { + function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { + Honk.VerificationKey memory vk = Honk.VerificationKey({ + circuitSize: uint256(262144), + logCircuitSize: uint256(18), + publicInputsSize: uint256(12), + ql: Honk.G1Point({ + x: uint256(0x2584b10bdc487b63d00fc5c54d9a63a1ccc756021101de92d558a5f603155f07), + y: uint256(0x09693eb2fbeed469aa74f03e6766304ff2b356561cef45afa8945fd6c0b47b1e) + }), + qr: Honk.G1Point({ + x: uint256(0x0c616de2a01a6c20364a1e6e23bc250356ff057e91eeb688ae6f5131bd5acdb1), + y: uint256(0x1b296b55b6970d2331e54b8168fb8a48bd0aaa1cbd8f1f4a211c6101b873548d) + }), + qo: Honk.G1Point({ + x: uint256(0x0167254d55367a14c55e48496d9f3fa8fd38b310633537090186bf7831d9b718), + y: uint256(0x0df9425cf38839d885495e406b91de39029b357b50e59fef44255fbd02dffef7) + }), + q4: Honk.G1Point({ + x: uint256(0x2e2af8c8fe5ceb6f08922daab1588779f2b9da625364cca35be40fa19a6fdfcd), + y: uint256(0x0d458cb3cbe7ce0053fab0f4601a9e72160d3c22da60832a99d56746cc83fe41) + }), + qm: Honk.G1Point({ + x: uint256(0x1a880da6b8102284a66588e4a5bc07463241198f85d30c3f7bb919b716b33159), + y: uint256(0x1da24162490251b2936681b8f43f716adf7a236721b29108642010c2239ddc4c) + }), + qc: Honk.G1Point({ + x: uint256(0x1959a2fa5fed15996e8d64b59086359d56f86df34aa505cfdd90859dfd38914c), + y: uint256(0x2a6a3b864455eb9c35cd0a76e26e739d77fdb909377f6950d7e861cb353d945f) + }), + qLookup: Honk.G1Point({ + x: uint256(0x13129f0714c3307644809495e0150426c814cd7c5e1ba2094969bb1d74f1c66b), + y: uint256(0x0e7d5873232b1bdd0ce181513b47d17d4549a4df958fe4825e7cb590563154ab) + }), + qArith: Honk.G1Point({ + x: uint256(0x134f7c3c39a63e36a48be27ac7b7d5d361b4eee600f6869d9ee73dc900c056ae), + y: uint256(0x2656d0b7747c32812906f370b1afcc40e0e5a85755195b7946c82adcad5075c7) + }), + qDeltaRange: Honk.G1Point({ + x: uint256(0x018516b01acc1336a51efc01add6b06aaf8e60ad26837e1ca8bd0a3057223c5e), + y: uint256(0x2c0e6011c3e04f3416b00832091d09ebf1411bfaf52a9118b233804a7612092f) + }), + qElliptic: Honk.G1Point({ + x: uint256(0x05d32ff4a57a86e0cf0bf1a61d5190d03f4381d02ddb8f65bb4cebd48ebd26f1), + y: uint256(0x1cd372cf8463f3efdfe3a771b64c8a560236372ba8cd7d210e9a45cab88fbaec) + }), + qMemory: Honk.G1Point({ + x: uint256(0x231c00f8f7bdb165cfd4244ed9a424ba077c7342fff07327e197f0d55f61c0c9), + y: uint256(0x0f852f60396805167a490e40b281eaa0fe1728e125793726c16ed3a8f265e547) + }), + qNnf: Honk.G1Point({ + x: uint256(0x00aac3eef85d3f77c117e1f078a521c4a779c8909bc751bd39965be3eb57fc93), + y: uint256(0x049c179522b22848c853f6ab21a95900c98a5d4b0960e75e9e014abe85cbc151) + }), + qPoseidon2External: Honk.G1Point({ + x: uint256(0x15682320e9a429e3b38ec86ea5b9864ad0f8a0c50c35d1cda552443911b76b18), + y: uint256(0x27a9340666d2eae207fc5f469dc44c336de2ca8c0101e249950ce9d77f2e3523) + }), + qPoseidon2Internal: Honk.G1Point({ + x: uint256(0x0eb43a728eaa089574b88361296124444d8d8a71295e0fb7979c4ce1d4f34150), + y: uint256(0x048f751da8cde70d68e841d88571970359b99a8939e7adb519aa6e1c3d3b1f70) + }), + s1: Honk.G1Point({ + x: uint256(0x2c3b4f0198404a7dd24abce7dd9a8403089a715e0eef352d355d73de3b6cfd78), + y: uint256(0x0c6008ba18bbae437798a708686c477ba05e51195618bf87804be6ef2e260cb9) + }), + s2: Honk.G1Point({ + x: uint256(0x16d994d4c365ca52223f3d65df7f14cda61c682350287adf9650e0c71b8bd466), + y: uint256(0x159fd47212a8bfe5da402e9293f7445d13eb7b04a20ea097c70d8f2187cf9984) + }), + s3: Honk.G1Point({ + x: uint256(0x115fe6f2d132d5bdddb959fd99cc0d949612bb6c3ce3274ce54348283e0583cc), + y: uint256(0x15be79eabef0cb6ad7c4084bc88a822e7c76376fa811e98fbd7fd2461b7ebda6) + }), + s4: Honk.G1Point({ + x: uint256(0x17f11af620cc8ae6fa7f591baf4538659fad460dc2c7dfa15ba12c8b3dbbae92), + y: uint256(0x2a5e38648cbdd5edc0d7d506a3e0a185e7ff26e3d8f660b8cd788aef881bb036) + }), + t1: Honk.G1Point({ + x: uint256(0x099e3bd5a0a00ab7fe18040105b9b395b5d8b7b4a63b05df652b0d10ef146d26), + y: uint256(0x0015b8d2515d76e2ccec99dcd194592129af3a637f5a622a32440f860d1e2a7f) + }), + t2: Honk.G1Point({ + x: uint256(0x1b917517920bad3d8bc01c9595092a222b888108dc25d1aa450e0b4bc212c37e), + y: uint256(0x305e8992b148eedb22e6e992077a84482141c7ebe42000a1d58ccb74381f6d19) + }), + t3: Honk.G1Point({ + x: uint256(0x16465a5ccbb550cd2c63bd58116fe47c86847618681dc29d8a9363ab7c40e1c3), + y: uint256(0x2e24d420fbf9508ed31de692db477b439973ac12d7ca796d6fe98ca40e6ca6b7) + }), + t4: Honk.G1Point({ + x: uint256(0x043d063b130adfb37342af45d0155a28edd1a7e46c840d9c943fdf45521c64ce), + y: uint256(0x261522c4089330646aff96736194949330952ae74c573d1686d9cb4a00733854) + }), + id1: Honk.G1Point({ + x: uint256(0x11a2b02a07833f4eb52acbc575a29627a4967a894a48e85aa1be196e4f8f27ad), + y: uint256(0x23089c3f8203e3c329603b83eda0be7aa27ae0aafe1c1adb238d7468fdc182ea) + }), + id2: Honk.G1Point({ + x: uint256(0x163e021e501f7fe34e4c1a5b1f952e8ba8e994f22af844ec8c5f1b5adbb81d3c), + y: uint256(0x11e0bde4ff7ded8e481f2725a8c7b77639daca3000460530342492a1144e1366) + }), + id3: Honk.G1Point({ + x: uint256(0x01aafb56c898052889057480543a507705cc11948d5f92e4701d40cc15753740), + y: uint256(0x2b18e5b09f5ae79a76a25fc5feb4be9fccb4371ea897b7202642caca04c52598) + }), + id4: Honk.G1Point({ + x: uint256(0x24b62717ca019c30fa893a939017835c3fd0c55341c1d0a5145daf7795a7e461), + y: uint256(0x1fd05e72c892221a0b2d0d338e82bc3203596804926d298d3e27263680228ee8) + }), + lagrangeFirst: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000001), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000002) + }), + lagrangeLast: Honk.G1Point({ + x: uint256(0x16bb8b1283dbb9b8889a5d5d2dd3b45573c25b0a8c4db2a2591d7b7b95560d98), + y: uint256(0x0edf6541fb9f55a4ab12876187640183878a77571170c6383f131e0d1328e0ab) + }) + }); + return vk; + } +} + +pragma solidity ^0.8.27; + +interface IVerifier { + function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); +} + +/** + * @notice Library of error codes + * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier + */ +library Errors { + error ValueGeLimbMax(); + error ValueGeGroupOrder(); + error ValueGeFieldOrder(); + + error InvertOfZero(); + error NotPowerOfTwo(); + error ModExpFailed(); + + error ProofLengthWrong(); + error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); + error PublicInputsLengthWrong(); + error SumcheckFailed(); + error ShpleminiFailed(); + + error PointAtInfinity(); + + error ConsistencyCheckFailed(); + error GeminiChallengeInSubgroup(); +} + +type Fr is uint256; + +using {add as +} for Fr global; +using {sub as -} for Fr global; +using {mul as *} for Fr global; + +using {notEqual as !=} for Fr global; +using {equal as ==} for Fr global; + +uint256 constant SUBGROUP_SIZE = 256; +uint256 constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Prime field order +uint256 constant P = MODULUS; +Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); +Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); +Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); +Fr constant ONE = Fr.wrap(1); +Fr constant ZERO = Fr.wrap(0); +// Instantiation + +library FrLib { + bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; + + function invert(Fr value) internal view returns (Fr) { + uint256 v = Fr.unwrap(value); + require(v != 0, Errors.InvertOfZero()); + + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), v) + mstore(add(free, 0x80), sub(MODULUS, 2)) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function pow(Fr base, uint256 v) internal view returns (Fr) { + uint256 b = Fr.unwrap(base); + // Only works for power of 2 + require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), b) + mstore(add(free, 0x80), v) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function div(Fr numerator, Fr denominator) internal view returns (Fr) { + unchecked { + return numerator * invert(denominator); + } + } + + function sqr(Fr value) internal pure returns (Fr) { + unchecked { + return value * value; + } + } + + function unwrap(Fr value) internal pure returns (uint256) { + unchecked { + return Fr.unwrap(value); + } + } + + function neg(Fr value) internal pure returns (Fr) { + unchecked { + return Fr.wrap(MODULUS - Fr.unwrap(value)); + } + } + + function from(uint256 value) internal pure returns (Fr) { + unchecked { + require(value < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(value); + } + } + + function fromBytes32(bytes32 value) internal pure returns (Fr) { + unchecked { + uint256 v = uint256(value); + require(v < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(v); + } + } + + function toBytes32(Fr value) internal pure returns (bytes32) { + unchecked { + return bytes32(Fr.unwrap(value)); + } + } +} + +// Free functions +function add(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function mul(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function sub(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); + } +} + +function notEqual(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) != Fr.unwrap(b); + } +} + +function equal(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) == Fr.unwrap(b); + } +} + +uint256 constant CONST_PROOF_SIZE_LOG_N = 25; + +uint256 constant NUMBER_OF_SUBRELATIONS = 28; +uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; +uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; +uint256 constant NUMBER_OF_ENTITIES = 41; +// The number of entities added for ZK (gemini_masking_poly) +uint256 constant NUM_MASKING_POLYNOMIALS = 1; +uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_UNSHIFTED = 36; +uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_TO_BE_SHIFTED = 5; +uint256 constant PAIRING_POINTS_SIZE = 8; + +uint256 constant FIELD_ELEMENT_SIZE = 0x20; +uint256 constant GROUP_ELEMENT_SIZE = 0x40; + +// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) +uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; + +// ENUM FOR WIRES +enum WIRE { + Q_M, + Q_C, + Q_L, + Q_R, + Q_O, + Q_4, + Q_LOOKUP, + Q_ARITH, + Q_RANGE, + Q_ELLIPTIC, + Q_MEMORY, + Q_NNF, + Q_POSEIDON2_EXTERNAL, + Q_POSEIDON2_INTERNAL, + SIGMA_1, + SIGMA_2, + SIGMA_3, + SIGMA_4, + ID_1, + ID_2, + ID_3, + ID_4, + TABLE_1, + TABLE_2, + TABLE_3, + TABLE_4, + LAGRANGE_FIRST, + LAGRANGE_LAST, + W_L, + W_R, + W_O, + W_4, + Z_PERM, + LOOKUP_INVERSES, + LOOKUP_READ_COUNTS, + LOOKUP_READ_TAGS, + W_L_SHIFT, + W_R_SHIFT, + W_O_SHIFT, + W_4_SHIFT, + Z_PERM_SHIFT +} + +library Honk { + struct G1Point { + uint256 x; + uint256 y; + } + + struct VerificationKey { + // Misc Params + uint256 circuitSize; + uint256 logCircuitSize; + uint256 publicInputsSize; + // Selectors + G1Point qm; + G1Point qc; + G1Point ql; + G1Point qr; + G1Point qo; + G1Point q4; + G1Point qLookup; // Lookup + G1Point qArith; // Arithmetic widget + G1Point qDeltaRange; // Delta Range sort + G1Point qMemory; // Memory + G1Point qNnf; // Non-native Field + G1Point qElliptic; // Auxillary + G1Point qPoseidon2External; + G1Point qPoseidon2Internal; + // Copy constraints + G1Point s1; + G1Point s2; + G1Point s3; + G1Point s4; + // Copy identity + G1Point id1; + G1Point id2; + G1Point id3; + G1Point id4; + // Precomputed lookup table + G1Point t1; + G1Point t2; + G1Point t3; + G1Point t4; + // Fixed first and last + G1Point lagrangeFirst; + G1Point lagrangeLast; + } + + struct RelationParameters { + // challenges + Fr eta; + Fr beta; + Fr gamma; + // derived + Fr publicInputsDelta; + } + + struct Proof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // Free wires + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Lookup helpers - Permutations + G1Point zPerm; + // Lookup helpers - logup + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Sumcheck + Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + G1Point shplonkQ; + G1Point kzgQuotient; + } + + /// forge-lint: disable-next-item(pascal-case-struct) + struct ZKProof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) + G1Point geminiMaskingPoly; + // Commitments to wire polynomials + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Commitments to logup witness polynomials + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Commitment to grand permutation polynomial + G1Point zPerm; + G1Point[3] libraCommitments; + // Sumcheck + Fr libraSum; + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr libraEvaluation; + Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + Fr[4] libraPolyEvals; + G1Point shplonkQ; + G1Point kzgQuotient; + } +} + +// ZKTranscript library to generate fiat shamir challenges, the ZK transcript only differest +/// forge-lint: disable-next-item(pascal-case-struct) +struct ZKTranscript { + // Oink + Honk.RelationParameters relationParameters; + Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] + Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; + // Sumcheck + Fr libraChallenge; + Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; + // Shplemini + Fr rho; + Fr geminiR; + Fr shplonkNu; + Fr shplonkZ; + // Derived + Fr publicInputsDelta; +} + +library ZKTranscriptLib { + function generateTranscript( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + uint256 logN + ) external pure returns (ZKTranscript memory t) { + Fr previousChallenge; + (t.relationParameters, previousChallenge) = + generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); + + (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); + + (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); + (t.libraChallenge, previousChallenge) = generateLibraChallenge(previousChallenge, proof); + (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); + + (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); + + (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); + + (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); + + (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); + return t; + } + + function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { + uint256 challengeU256 = uint256(Fr.unwrap(challenge)); + // Split into two equal 127-bit chunks (254/2) + uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits + uint256 hi = challengeU256 >> 127; + first = FrLib.from(lo); + second = FrLib.from(hi); + } + + function generateRelationParametersChallenges( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + Fr previousChallenge + ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { + (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); + + (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); + } + + function generateEtaChallenge( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize + ) internal pure returns (Fr eta, Fr previousChallenge) { + // Size: 1 (vkHash) + publicInputsSize + 8 (geminiMask(2) + 3 wires(6)) + bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 8); + round0[0] = bytes32(vkHash); + + for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { + require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); + round0[1 + i] = publicInputs[i]; + } + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); + } + + // For ZK flavors: hash the gemini masking poly commitment (sent right after public inputs) + round0[1 + publicInputsSize] = bytes32(proof.geminiMaskingPoly.x); + round0[1 + publicInputsSize + 1] = bytes32(proof.geminiMaskingPoly.y); + + // Create the first challenge + // Note: w4 is added to the challenge later on + round0[1 + publicInputsSize + 2] = bytes32(proof.w1.x); + round0[1 + publicInputsSize + 3] = bytes32(proof.w1.y); + round0[1 + publicInputsSize + 4] = bytes32(proof.w2.x); + round0[1 + publicInputsSize + 5] = bytes32(proof.w2.y); + round0[1 + publicInputsSize + 6] = bytes32(proof.w3.x); + round0[1 + publicInputsSize + 7] = bytes32(proof.w3.y); + + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); + (eta,) = splitChallenge(previousChallenge); + } + + function generateBetaGammaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) + { + bytes32[7] memory round1; + round1[0] = FrLib.toBytes32(previousChallenge); + round1[1] = bytes32(proof.lookupReadCounts.x); + round1[2] = bytes32(proof.lookupReadCounts.y); + round1[3] = bytes32(proof.lookupReadTags.x); + round1[4] = bytes32(proof.lookupReadTags.y); + round1[5] = bytes32(proof.w4.x); + round1[6] = bytes32(proof.w4.y); + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); + (beta, gamma) = splitChallenge(nextPreviousChallenge); + } + + // Alpha challenges non-linearise the gate contributions + function generateAlphaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) + { + // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup + uint256[5] memory alpha0; + alpha0[0] = Fr.unwrap(previousChallenge); + alpha0[1] = proof.lookupInverses.x; + alpha0[2] = proof.lookupInverses.y; + alpha0[3] = proof.zPerm.x; + alpha0[4] = proof.zPerm.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); + Fr alpha; + (alpha,) = splitChallenge(nextPreviousChallenge); + + // Compute powers of alpha for batching subrelations + alphas[0] = alpha; + for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { + alphas[i] = alphas[i - 1] * alpha; + } + } + + function generateGateChallenges(Fr previousChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) + { + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); + (gateChallenges[0],) = splitChallenge(previousChallenge); + for (uint256 i = 1; i < logN; i++) { + gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; + } + nextPreviousChallenge = previousChallenge; + } + + function generateLibraChallenge(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr libraChallenge, Fr nextPreviousChallenge) + { + // 2 comm, 1 sum, 1 challenge + uint256[4] memory challengeData; + challengeData[0] = Fr.unwrap(previousChallenge); + challengeData[1] = proof.libraCommitments[0].x; + challengeData[2] = proof.libraCommitments[0].y; + challengeData[3] = Fr.unwrap(proof.libraSum); + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(challengeData))) % P); + (libraChallenge,) = splitChallenge(nextPreviousChallenge); + } + + function generateSumcheckChallenges(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) + { + for (uint256 i = 0; i < logN; i++) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; + univariateChal[0] = prevChallenge; + + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; + } + prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); + + (sumcheckChallenges[i],) = splitChallenge(prevChallenge); + } + nextPreviousChallenge = prevChallenge; + } + + // We add Libra claimed eval + 2 libra commitments (grand_sum, quotient) + function generateRhoChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr rho, Fr nextPreviousChallenge) + { + uint256[NUMBER_OF_ENTITIES_ZK + 6] memory rhoChallengeElements; + rhoChallengeElements[0] = Fr.unwrap(prevChallenge); + uint256 i; + for (i = 1; i <= NUMBER_OF_ENTITIES_ZK; i++) { + rhoChallengeElements[i] = Fr.unwrap(proof.sumcheckEvaluations[i - 1]); + } + rhoChallengeElements[i] = Fr.unwrap(proof.libraEvaluation); + i += 1; + rhoChallengeElements[i] = proof.libraCommitments[1].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[1].y; + i += 2; + rhoChallengeElements[i] = proof.libraCommitments[2].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[2].y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); + (rho,) = splitChallenge(nextPreviousChallenge); + } + + function generateGeminiRChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr geminiR, Fr nextPreviousChallenge) + { + uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); + gR[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 0; i < logN - 1; i++) { + gR[1 + i * 2] = proof.geminiFoldComms[i].x; + gR[2 + i * 2] = proof.geminiFoldComms[i].y; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); + + (geminiR,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkNuChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr shplonkNu, Fr nextPreviousChallenge) + { + uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1 + 4); + shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 1; i <= logN; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.geminiAEvaluations[i - 1]); + } + + uint256 libraIdx = 0; + for (uint256 i = logN + 1; i <= logN + 4; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.libraPolyEvals[libraIdx]); + libraIdx++; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); + (shplonkNu,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkZChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr shplonkZ, Fr nextPreviousChallenge) + { + uint256[3] memory shplonkZChallengeElements; + shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); + + shplonkZChallengeElements[1] = proof.shplonkQ.x; + shplonkZChallengeElements[2] = proof.shplonkQ.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); + (shplonkZ,) = splitChallenge(nextPreviousChallenge); + } + + function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.ZKProof memory p) { + uint256 boundary = 0x0; + + // Pairing point object + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); + // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 + require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); + p.pairingPointObject[i] = FrLib.from(limb); + boundary += FIELD_ELEMENT_SIZE; + } + + // Gemini masking polynomial commitment (sent first in ZK flavors, right after pairing points) + p.geminiMaskingPoly = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Commitments + p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Lookup / Permutation Helper Commitments + p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[0] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + p.libraSum = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + // Sumcheck univariates + for (uint256 i = 0; i < logN; i++) { + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + } + + // Sumcheck evaluations (includes gemini_masking_poly eval at index 0 for ZK flavors) + for (uint256 i = 0; i < NUMBER_OF_ENTITIES_ZK; i++) { + p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + p.libraEvaluation = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + + p.libraCommitments[1] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[2] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Gemini + // Read gemini fold univariates + for (uint256 i = 0; i < logN - 1; i++) { + p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + } + + // Read gemini a evaluations + for (uint256 i = 0; i < logN; i++) { + p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + for (uint256 i = 0; i < 4; i++) { + p.libraPolyEvals[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + // Shplonk + p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + // KZG + p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + } +} + +library RelationsLib { + struct EllipticParams { + // Points + Fr x_1; + Fr y_1; + Fr x_2; + Fr y_2; + Fr y_3; + Fr x_3; + // push accumulators into memory + Fr x_double_identity; + } + + // Parameters used within the Memory Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct MemParams { + Fr memory_record_check; + Fr partial_record_check; + Fr next_gate_access_type; + Fr record_delta; + Fr index_delta; + Fr adjacent_values_match_if_adjacent_indices_match; + Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; + Fr access_check; + Fr next_gate_access_type_is_boolean; + Fr ROM_consistency_check_identity; + Fr RAM_consistency_check_identity; + Fr timestamp_delta; + Fr RAM_timestamp_check_identity; + Fr memory_identity; + Fr index_is_monotonically_increasing; + } + + // Parameters used within the Non-Native Field Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct NnfParams { + Fr limb_subproduct; + Fr non_native_field_gate_1; + Fr non_native_field_gate_2; + Fr non_native_field_gate_3; + Fr limb_accumulator_1; + Fr limb_accumulator_2; + Fr nnf_identity; + } + + struct PoseidonExternalParams { + Fr s1; + Fr s2; + Fr s3; + Fr s4; + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr t0; + Fr t1; + Fr t2; + Fr t3; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr q_pos_by_scaling; + } + + struct PoseidonInternalParams { + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr u_sum; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr s1; + Fr q_pos_by_scaling; + } + + Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) + uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; + + // Constants for the Non-native Field relation + Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); + Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); + + function accumulateRelationEvaluations( + Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, + Fr powPartialEval + ) internal pure returns (Fr accumulator) { + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; + + // Accumulate all relations in Ultra Honk - each with varying number of subrelations + accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); + + // batch the subrelations with the precomputed alpha powers to obtain the full honk relation + accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); + } + + /** + * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids + * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code + * editors, and thus is noisy. + */ + function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { + return p[uint256(_wire)]; + } + + /** + * Ultra Arithmetic Relation + * + */ + function accumulateArithmeticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + // Relation 0 + Fr q_arith = wire(p, WIRE.Q_ARITH); + { + Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); + + Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; + accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) + + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); + accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); + accum = accum * q_arith; + accum = accum * domainSep; + evals[0] = accum; + } + + // Relation 1 + { + Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); + accum = accum * (q_arith - Fr.wrap(2)); + accum = accum * (q_arith - ONE); + accum = accum * q_arith; + accum = accum * domainSep; + evals[1] = accum; + } + } + + function accumulatePermutationRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr grand_product_numerator; + Fr grand_product_denominator; + + { + Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; + num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); + + grand_product_numerator = num; + } + { + Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; + den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); + + grand_product_denominator = den; + } + + // Contribution 2 + { + Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; + + acc = acc + - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) + * grand_product_denominator); + acc = acc * domainSep; + evals[2] = acc; + } + + // Contribution 3 + { + Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; + evals[3] = acc; + } + } + + function accumulateLogDerivativeLookupRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr table_term; + Fr lookup_term; + + // Calculate the write term (the table accumulation) + // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) + + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); + } + + // Calculate the read term + // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); + Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); + Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); + + lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) + + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); + } + + Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; + Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; + + Fr inverse_exists_xor = + wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) + - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); + + // Inverse calculated correctly relation + Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; + accumulatorNone = accumulatorNone * domainSep; + + // Inverse + Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; + + Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); + + Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; + + evals[4] = accumulatorNone; + evals[5] = accumulatorOne; + evals[6] = read_tag_boolean_relation * domainSep; + } + + function accumulateDeltaRangeRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr minus_one = ZERO - ONE; + Fr minus_two = ZERO - Fr.wrap(2); + Fr minus_three = ZERO - Fr.wrap(3); + + // Compute wire differences + Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); + Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); + Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); + Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); + + // Contribution 6 + { + Fr acc = delta_1; + acc = acc * (delta_1 + minus_one); + acc = acc * (delta_1 + minus_two); + acc = acc * (delta_1 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[7] = acc; + } + + // Contribution 7 + { + Fr acc = delta_2; + acc = acc * (delta_2 + minus_one); + acc = acc * (delta_2 + minus_two); + acc = acc * (delta_2 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[8] = acc; + } + + // Contribution 8 + { + Fr acc = delta_3; + acc = acc * (delta_3 + minus_one); + acc = acc * (delta_3 + minus_two); + acc = acc * (delta_3 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[9] = acc; + } + + // Contribution 9 + { + Fr acc = delta_4; + acc = acc * (delta_4 + minus_one); + acc = acc * (delta_4 + minus_two); + acc = acc * (delta_4 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[10] = acc; + } + } + + function accumulateEllipticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + EllipticParams memory ep; + ep.x_1 = wire(p, WIRE.W_R); + ep.y_1 = wire(p, WIRE.W_O); + + ep.x_2 = wire(p, WIRE.W_L_SHIFT); + ep.y_2 = wire(p, WIRE.W_4_SHIFT); + ep.y_3 = wire(p, WIRE.W_O_SHIFT); + ep.x_3 = wire(p, WIRE.W_R_SHIFT); + + Fr q_sign = wire(p, WIRE.Q_L); + Fr q_is_double = wire(p, WIRE.Q_M); + + // Contribution 10 point addition, x-coordinate check + // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 + Fr x_diff = (ep.x_2 - ep.x_1); + Fr y1_sqr = (ep.y_1 * ep.y_1); + { + // Move to top + Fr partialEval = domainSep; + + Fr y2_sqr = (ep.y_2 * ep.y_2); + Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; + Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); + x_add_identity = x_add_identity * x_diff * x_diff; + x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; + + evals[11] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 11 point addition, x-coordinate check + // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 + { + Fr y1_plus_y3 = ep.y_1 + ep.y_3; + Fr y_diff = ep.y_2 * q_sign - ep.y_1; + Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; + evals[12] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 10 point doubling, x-coordinate check + // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 + // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 + { + Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; + Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; + y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; + Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); + + // NOTE: pushed into memory (stack >:'( ) + ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; + + Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + evals[11] = evals[11] + acc; + } + + // Contribution 11 point doubling, y-coordinate check + // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 + { + Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; + Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); + evals[12] = evals[12] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + } + } + + function accumulateMemoryRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + MemParams memory ap; + + // Compute eta powers locally + Fr eta_two = rp.eta * rp.eta; + Fr eta_three = eta_two * rp.eta; + + /** + * MEMORY + * + * A RAM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) + * * v: `value` of memory cell being accessed + * * a: `access` type of record. read: 0 = read, 1 = write + * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three + * + * A ROM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three + * + * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + + * selectors, depending on whether the gate is a RAM read/write or a ROM read + * + * | gate type | i | v2/t | v | a | r | + * | --------- | -- | ----- | -- | -- | -- | + * | ROM | w1 | w2 | w3 | -- | w4 | + * | RAM | w1 | w2 | w3 | qc | w4 | + * + * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on + * `w2` to fix its value) + * + * + */ + + /** + * Memory Record Check + * Partial degree: 1 + * Total degree: 4 + * + * A ROM/ROM access gate can be evaluated with the identity: + * + * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 + * + * For ROM gates, qc = 0 + */ + ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); + ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); + ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 + ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); + + /** + * Contribution 13 & 14 + * ROM Consistency Check + * Partial degree: 1 + * Total degree: 4 + * + * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of + * records that are sorted. + * + * We apply the following checks for the sorted records: + * + * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 + * 2. index values for adjacent records are monotonically increasing + * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} + * + */ + ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); + ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); + + ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 + + ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 + + evals[14] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + evals[15] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + + ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 + + /** + * Contributions 15,16,17 + * RAM Consistency Check + * + * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` + * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. + * This is validated by requiring `access` to be boolean + * + * For two adjacent entries in the sorted list if _both_ + * A) index values match + * B) adjacent access value is 0 (i.e. next gate is a READ) + * then + * C) both values must match. + * The gate boolean check is + * (A && B) => C === !(A && B) || C === !A || !B || C + * + * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized + * with a WRITE operation. + */ + Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 + ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 + + // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta + // deg 1 or 4 + ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); + ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; + + Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); + ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = + (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 + + // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the + // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't + // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access + // type is correct, to cover this edge case + // deg 2 or 4 + ap.next_gate_access_type_is_boolean = + ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; + + // Putting it all together... + evals[16] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation + * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 + evals[17] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 + evals[18] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 6 + + ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 + + /** + * RAM Timestamp Consistency Check + * + * | w1 | w2 | w3 | w4 | + * | index | timestamp | timestamp_check | -- | + * + * Let delta_index = index_{i + 1} - index_{i} + * + * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i + * Else timestamp_check = 0 + */ + ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); + ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg 3 + + /** + * Complete Contribution 12 + * The complete RAM/ROM memory identity + * Partial degree: + */ + ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 + ap.memory_identity = + ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg 4 + ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 or 6 + ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 + + // (deg 3 or 9) + (deg 4) + (deg 3) + ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 + evals[13] = ap.memory_identity; + } + + function accumulateNnfRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + NnfParams memory ap; + + /** + * Contribution 12 + * Non native field arithmetic gate 2 + * deg 4 + * + * _ _ + * / _ _ _ 14 \ + * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | + * \_ _/ + * + * + */ + ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); + ap.non_native_field_gate_2 = + (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); + + ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; + ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); + ap.non_native_field_gate_1 = ap.limb_subproduct; + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); + + ap.non_native_field_gate_3 = ap.limb_subproduct; + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); + + Fr non_native_field_identity = + ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; + non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); + + // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm + // deg 2 + ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); + ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); + + // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm + // deg 2 + ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); + ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); + + Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; + limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 + + ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; + ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); + evals[19] = ap.nnf_identity; + } + + function accumulatePoseidonExternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonExternalParams memory ep; + + ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); + ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); + ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); + + ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; + ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; + ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; + ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; + // matrix mul v = M_E * u with 14 additions + ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 + ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 + ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 + // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 + ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 + // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 + ep.v4 = ep.t1 + ep.t1; + ep.v4 = ep.v4 + ep.v4 + ep.t3; + // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 + ep.v2 = ep.t0 + ep.t0; + ep.v2 = ep.v2 + ep.v2 + ep.t2; + // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 + ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 + ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 + + ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; + evals[20] = evals[20] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); + + evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); + + evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); + + evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + function accumulatePoseidonInternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonInternalParams memory ip; + + Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ + FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), + FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), + FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), + FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) + ]; + + // add round constants + ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + + // apply s-box round + ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; + ip.u2 = wire(p, WIRE.W_R); + ip.u3 = wire(p, WIRE.W_O); + ip.u4 = wire(p, WIRE.W_4); + + // matrix mul with v = M_I * u 4 muls and 7 additions + ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; + + ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; + + ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; + evals[24] = evals[24] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); + + ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; + evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); + + ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; + evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); + + ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; + evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + // Batch subrelation evaluations using precomputed powers of alpha + // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array + function scaleAndBatchSubrelations( + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges + ) internal pure returns (Fr accumulator) { + accumulator = evaluations[0]; + + for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { + accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; + } + } +} + +library CommitmentSchemeLib { + using FrLib for Fr; + + // Avoid stack too deep + struct ShpleminiIntermediates { + Fr unshiftedScalar; + Fr shiftedScalar; + Fr unshiftedScalarNeg; + Fr shiftedScalarNeg; + // Scalar to be multiplied by [1]₁ + Fr constantTermAccumulator; + // Accumulator for powers of rho + Fr batchingChallenge; + // Linear combination of multilinear (sumcheck) evaluations and powers of rho + Fr batchedEvaluation; + Fr[4] denominators; + Fr[4] batchingScalars; + // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr posInvertedDenominator; + // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr negInvertedDenominator; + // ν^{2i} * 1/(z - r^{2^i}) + Fr scalingFactorPos; + // ν^{2i+1} * 1/(z + r^{2^i}) + Fr scalingFactorNeg; + // Fold_i(r^{2^i}) reconstructed by Verifier + Fr[] foldPosEvaluations; + } + + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 + function computeFoldPosEvaluations( + Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, + Fr batchedEvalAccumulator, + Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, + Fr[] memory geminiEvalChallengePowers, + uint256 logSize + ) internal view returns (Fr[] memory) { + Fr[] memory foldPosEvaluations = new Fr[](logSize); + for (uint256 i = logSize; i > 0; --i) { + Fr challengePower = geminiEvalChallengePowers[i - 1]; + Fr u = sumcheckUChallenges[i - 1]; + + Fr batchedEvalRoundAcc = ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] + * (challengePower * (ONE - u) - u)); + // Divide by the denominator + batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); + + batchedEvalAccumulator = batchedEvalRoundAcc; + foldPosEvaluations[i - 1] = batchedEvalRoundAcc; + } + return foldPosEvaluations; + } + + function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { + Fr[] memory squares = new Fr[](logN); + squares[0] = r; + for (uint256 i = 1; i < logN; ++i) { + squares[i] = squares[i - 1].sqr(); + } + return squares; + } +} + +uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // EC group order. F_q + +// Fr utility + +function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { + scalar = FrLib.fromBytes32(bytes32(proofSection)); +} + +// EC Point utilities +function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { + uint256 x = uint256(bytes32(proofSection[0x00:0x20])); + uint256 y = uint256(bytes32(proofSection[0x20:0x40])); + require(x < Q && y < Q, Errors.ValueGeGroupOrder()); + + // Reject the point at infinity (0,0). EVM precompiles silently treat (0,0) + // as the identity element, which could zero out commitments. + // On-curve validation (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles + // per EIP-196, so we only need to catch this special case here. + require((x | y) != 0, Errors.PointAtInfinity()); + + point = Honk.G1Point({x: x, y: y}); +} + +function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { + // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. + if (point.y != 0) { + point.y = Q - point.y; + } + return point; +} + +/** + * Convert the pairing points to G1 points. + * + * The pairing points are serialised as an array of 2 limbs representing two points + * (P0 and P1, used for lhs and rhs of pairing operation). + * + * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. + * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] + * + * @param pairingPoints The pairing points to convert. + * @return lhs P0 point + * @return rhs P1 point + */ +function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) + pure + returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) +{ + // P0 (lhs): x = lo | (hi << 136) + uint256 lhsX = Fr.unwrap(pairingPoints[0]); + lhsX |= Fr.unwrap(pairingPoints[1]) << 136; + + uint256 lhsY = Fr.unwrap(pairingPoints[2]); + lhsY |= Fr.unwrap(pairingPoints[3]) << 136; + + // P1 (rhs): x = lo | (hi << 136) + uint256 rhsX = Fr.unwrap(pairingPoints[4]); + rhsX |= Fr.unwrap(pairingPoints[5]) << 136; + + uint256 rhsY = Fr.unwrap(pairingPoints[6]); + rhsY |= Fr.unwrap(pairingPoints[7]) << 136; + + // Reconstructed coordinates must be < Q to prevent malleability. + // Without this, two different limb encodings could map to the same curve point + // (via mulmod reduction in on-curve checks) but produce different transcript hashes. + require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); + + lhs.x = lhsX; + lhs.y = lhsY; + rhs.x = rhsX; + rhs.y = rhsY; +} + +/** + * Hash the pairing inputs from the present verification context with those extracted from the public inputs. + * + * @param proofPairingPoints Pairing points from the proof - (public inputs). + * @param accLhs Accumulator point for the left side - result of shplemini. + * @param accRhs Accumulator point for the right side - result of shplemini. + * @return recursionSeparator The recursion separator - generated from hashing the above. + */ +function generateRecursionSeparator( + Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, + Honk.G1Point memory accLhs, + Honk.G1Point memory accRhs +) pure returns (Fr recursionSeparator) { + // hash the proof aggregated X + // hash the proof aggregated Y + // hash the accum X + // hash the accum Y + + (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); + + uint256[8] memory recursionSeparatorElements; + + // Proof points + recursionSeparatorElements[0] = proofLhs.x; + recursionSeparatorElements[1] = proofLhs.y; + recursionSeparatorElements[2] = proofRhs.x; + recursionSeparatorElements[3] = proofRhs.y; + + // Accumulator points + recursionSeparatorElements[4] = accLhs.x; + recursionSeparatorElements[5] = accLhs.y; + recursionSeparatorElements[6] = accRhs.x; + recursionSeparatorElements[7] = accRhs.y; + + recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); +} + +/** + * G1 Mul with Separator + * Using the ecAdd and ecMul precompiles + * + * @param basePoint The point to multiply. + * @param other The other point to add. + * @param recursionSeperator The separator to use for the multiplication. + * @return `(recursionSeperator * basePoint) + other`. + */ +function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) + view + returns (Honk.G1Point memory) +{ + Honk.G1Point memory result; + + result = ecMul(recursionSeperator, basePoint); + result = ecAdd(result, other); + + return result; +} + +/** + * G1 Mul + * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. + * + * @param value The value to multiply the point by. + * @param point The point to multiply. + * @return result The result of the multiplication. + */ +function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write the point into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | point.x + // free + 0x20| point.y + mstore(free, mload(point)) + mstore(add(free, 0x20), mload(add(point, 0x20))) + // Write the scalar into memory (one 32 byte word) + // Memory layout: + // Address | value + // free + 0x40| value + mstore(add(free, 0x40), value) + + // Call the ecMul precompile, it takes in the following + // [point.x, point.y, scalar], and returns the result back into the free memory location. + let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) + if iszero(success) { + revert(0, 0) + } + // Copy the result of the multiplication back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x60)) + } + + return result; +} + +/** + * G1 Add + * Takes two G1 points and uses the ecAdd precompile to return the result. + * + * @param lhs The left hand side of the addition. + * @param rhs The right hand side of the addition. + * @return result The result of the addition. + */ +function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write lhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | lhs.x + // free + 0x20| lhs.y + mstore(free, mload(lhs)) + mstore(add(free, 0x20), mload(add(lhs, 0x20))) + + // Write rhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free + 0x40| rhs.x + // free + 0x60| rhs.y + mstore(add(free, 0x40), mload(rhs)) + mstore(add(free, 0x60), mload(add(rhs, 0x20))) + + // Call the ecAdd precompile, it takes in the following + // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. + let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) + if iszero(success) { revert(0, 0) } + + // Copy the result of the addition back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x80)) + } + + return result; +} + +function rejectPointAtInfinity(Honk.G1Point memory point) pure { + require((point.x | point.y) != 0, Errors.PointAtInfinity()); +} + +/** + * Check if pairing point limbs are all zero (default/infinity). + * Default pairing points indicate no recursive verification occurred. + */ +function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { + uint256 acc = 0; + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + acc |= Fr.unwrap(pairingPoints[i]); + } + return acc == 0; +} + +function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { + bytes memory input = abi.encodePacked( + rhs.x, + rhs.y, + // Fixed G2 point + uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), + uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), + uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), + uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), + lhs.x, + lhs.y, + // G2 point from VK + uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), + uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), + uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), + uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) + ); + + (bool success, bytes memory result) = address(0x08).staticcall(input); + decodedResult = success && abi.decode(result, (bool)); +} + +abstract contract BaseZKHonkVerifier is IVerifier { + using FrLib for Fr; + + struct PairingInputs { + Honk.G1Point P_0; + Honk.G1Point P_1; + } + + struct SmallSubgroupIpaIntermediates { + Fr[SUBGROUP_SIZE] challengePolyLagrange; + Fr challengePolyEval; + Fr lagrangeFirst; + Fr lagrangeLast; + Fr rootPower; + Fr[SUBGROUP_SIZE] denominators; // this has to disappear + Fr diff; + } + + // Constants for proof length calculation (matching UltraKeccakZKFlavor) + uint256 internal constant NUM_WITNESS_ENTITIES = 8 + NUM_MASKING_POLYNOMIALS; + uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points + uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements + uint256 internal constant NUM_LIBRA_EVALUATIONS = 4; // libra evaluations + + uint256 internal constant LIBRA_COMMITMENTS = 3; + uint256 internal constant LIBRA_EVALUATIONS = 4; + uint256 internal constant LIBRA_UNIVARIATES_LENGTH = 9; + + uint256 internal constant SHIFTED_COMMITMENTS_START = 30; + uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; + + uint256 internal immutable $N; + uint256 internal immutable $LOG_N; + uint256 internal immutable $VK_HASH; + uint256 internal immutable $NUM_PUBLIC_INPUTS; + uint256 internal immutable $MSMSize; + + constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { + $N = _N; + $LOG_N = _logN; + $VK_HASH = _vkHash; + $NUM_PUBLIC_INPUTS = _numPublicInputs; + $MSMSize = NUMBER_UNSHIFTED_ZK + _logN + LIBRA_COMMITMENTS + 2; + } + + function verify(bytes calldata proof, bytes32[] calldata publicInputs) + public + view + override + returns (bool verified) + { + // Calculate expected proof size based on $LOG_N + uint256 expectedProofSize = calculateProofSize($LOG_N); + + // Check the received proof is the expected size where each field element is 32 bytes + require( + proof.length == expectedProofSize, Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize) + ); + + Honk.VerificationKey memory vk = loadVerificationKey(); + Honk.ZKProof memory p = ZKTranscriptLib.loadProof(proof, $LOG_N); + + require(publicInputs.length == vk.publicInputsSize - PAIRING_POINTS_SIZE, Errors.PublicInputsLengthWrong()); + + // Generate the fiat shamir challenges for the whole protocol + ZKTranscript memory t = + ZKTranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); + + // Derive public input delta + t.relationParameters.publicInputsDelta = computePublicInputDelta( + publicInputs, + p.pairingPointObject, + t.relationParameters.beta, + t.relationParameters.gamma, /*pubInputsOffset=*/ + 1 + ); + + // Sumcheck + require(verifySumcheck(p, t), Errors.SumcheckFailed()); + require(verifyShplemini(p, vk, t), Errors.ShpleminiFailed()); + + verified = true; + } + + function computePublicInputDelta( + bytes32[] memory publicInputs, + Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, + Fr beta, + Fr gamma, + uint256 offset + ) internal view returns (Fr publicInputDelta) { + Fr numerator = Fr.wrap(1); + Fr denominator = Fr.wrap(1); + + Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); + Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); + + { + for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { + Fr pubInput = FrLib.fromBytes32(publicInputs[i]); + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + Fr pubInput = pairingPointObject[i]; + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + } + + // Fr delta = numerator / denominator; // TOOO: batch invert later? + publicInputDelta = FrLib.div(numerator, denominator); + } + + function verifySumcheck(Honk.ZKProof memory proof, ZKTranscript memory tp) internal view returns (bool verified) { + Fr roundTargetSum = tp.libraChallenge * proof.libraSum; // default 0 + Fr powPartialEvaluation = Fr.wrap(1); + + // We perform sumcheck reductions over log n rounds ( the multivariate degree ) + for (uint256 round; round < $LOG_N; ++round) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; + Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; + require(totalSum == roundTargetSum, Errors.SumcheckFailed()); + + Fr roundChallenge = tp.sumCheckUChallenges[round]; + + // Update the round target for the next rounf + roundTargetSum = computeNextTargetSum(roundUnivariate, roundChallenge); + powPartialEvaluation = + powPartialEvaluation * (Fr.wrap(1) + roundChallenge * (tp.gateChallenges[round] - Fr.wrap(1))); + } + + // Last round + // For ZK flavors: sumcheckEvaluations has 42 elements + // Index 0 is gemini_masking_poly, indices 1-41 are the regular entities used in relations + Fr[NUMBER_OF_ENTITIES] memory relationsEvaluations; + for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { + relationsEvaluations[i] = proof.sumcheckEvaluations[i + NUM_MASKING_POLYNOMIALS]; // Skip gemini_masking_poly at index 0 + } + Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( + relationsEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation + ); + + Fr evaluation = Fr.wrap(1); + for (uint256 i = 2; i < $LOG_N; i++) { + evaluation = evaluation * tp.sumCheckUChallenges[i]; + } + + grandHonkRelationSum = + grandHonkRelationSum * (Fr.wrap(1) - evaluation) + proof.libraEvaluation * tp.libraChallenge; + verified = (grandHonkRelationSum == roundTargetSum); + } + + // Return the new target sum for the next sumcheck round + function computeNextTargetSum(Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) + internal + view + returns (Fr targetSum) + { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000240), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80) + ]; + + // To compute the next target sum, we evaluate the given univariate at a point u (challenge). + + // Performing Barycentric evaluations + // Compute B(x) + Fr numeratorValue = Fr.wrap(1); + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); + } + + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + denominatorInverses[i] = FrLib.invert(BARYCENTRIC_LAGRANGE_DENOMINATORS[i] * (roundChallenge - Fr.wrap(i))); + } + + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + targetSum = targetSum + roundUnivariates[i] * denominatorInverses[i]; + } + + // Scale the sum by the value of B(x) + targetSum = targetSum * numeratorValue; + } + + function verifyShplemini(Honk.ZKProof memory proof, Honk.VerificationKey memory vk, ZKTranscript memory tp) + internal + view + returns (bool verified) + { + CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack + + // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size + Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); + // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings + Fr[] memory scalars = new Fr[]($MSMSize); + Honk.G1Point[] memory commitments = new Honk.G1Point[]($MSMSize); + + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); + + mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); + mem.shiftedScalar = + tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); + + scalars[0] = Fr.wrap(1); + commitments[0] = proof.shplonkQ; + + /* Batch multivariate opening claims, shifted and unshifted + * The vector of scalars is populated as follows: + * \f[ + * \left( + * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) + * \right) + * \f] + * + * The following vector is concatenated to the vector of commitments: + * \f[ + * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} + * \f] + * + * Simultaneously, the evaluation of the multilinear polynomial + * \f[ + * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} + * \f] + * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. + * + * This approach minimizes the number of iterations over the commitments to multilinear polynomials + * and eliminates the need to store the powers of \f$ \rho \f$. + */ + // For ZK flavors: evaluations array is [gemini_masking_poly, qm, qc, ql, qr, ...] + // Start batching challenge at 1, not rho, to match non-ZK pattern + mem.batchingChallenge = Fr.wrap(1); + mem.batchedEvaluation = Fr.wrap(0); + + mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); + mem.shiftedScalarNeg = mem.shiftedScalar.neg(); + + // Process all NUMBER_UNSHIFTED_ZK evaluations (includes gemini_masking_poly at index 0) + for (uint256 i = 1; i <= NUMBER_UNSHIFTED_ZK; ++i) { + scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; + mem.batchedEvaluation = mem.batchedEvaluation + + (proof.sumcheckEvaluations[i - NUM_MASKING_POLYNOMIALS] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + // g commitments are accumulated at r + // For each of the to be shifted commitments perform the shift in place by + // adding to the unshifted value. + // We do so, as the values are to be used in batchMul later, and as + // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. + // Applied to w1, w2, w3, w4 and zPerm + for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { + uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; + uint256 evaluationOff = i + NUMBER_UNSHIFTED_ZK; + + scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); + mem.batchedEvaluation = + mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + + commitments[1] = proof.geminiMaskingPoly; + + commitments[2] = vk.qm; + commitments[3] = vk.qc; + commitments[4] = vk.ql; + commitments[5] = vk.qr; + commitments[6] = vk.qo; + commitments[7] = vk.q4; + commitments[8] = vk.qLookup; + commitments[9] = vk.qArith; + commitments[10] = vk.qDeltaRange; + commitments[11] = vk.qElliptic; + commitments[12] = vk.qMemory; + commitments[13] = vk.qNnf; + commitments[14] = vk.qPoseidon2External; + commitments[15] = vk.qPoseidon2Internal; + commitments[16] = vk.s1; + commitments[17] = vk.s2; + commitments[18] = vk.s3; + commitments[19] = vk.s4; + commitments[20] = vk.id1; + commitments[21] = vk.id2; + commitments[22] = vk.id3; + commitments[23] = vk.id4; + commitments[24] = vk.t1; + commitments[25] = vk.t2; + commitments[26] = vk.t3; + commitments[27] = vk.t4; + commitments[28] = vk.lagrangeFirst; + commitments[29] = vk.lagrangeLast; + + // Accumulate proof points + commitments[30] = proof.w1; + commitments[31] = proof.w2; + commitments[32] = proof.w3; + commitments[33] = proof.w4; + commitments[34] = proof.zPerm; + commitments[35] = proof.lookupInverses; + commitments[36] = proof.lookupReadCounts; + commitments[37] = proof.lookupReadTags; + + /* Batch gemini claims from the prover + * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from + * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars + * + * 1. Moves the vector + * \f[ + * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) + * \f] + * to the 'commitments' vector. + * + * 2. Computes the scalars: + * \f[ + * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} + * \f] + * and places them into the 'scalars' vector. + * + * 3. Accumulates the summands of the constant term: + * \f[ + * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} + * \f] + * and adds them to the 'constant_term_accumulator'. + */ + + // Add contributions from A₀(r) and A₀(-r) to constant_term_accumulator: + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 + Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( + tp.sumCheckUChallenges, + mem.batchedEvaluation, + proof.geminiAEvaluations, + powers_of_evaluation_challenge, + $LOG_N + ); + + mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; + mem.constantTermAccumulator = + mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); + + mem.batchingChallenge = tp.shplonkNu.sqr(); + uint256 boundary = NUMBER_UNSHIFTED_ZK + 1; + + // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; + // Compute scalar multipliers for each fold commitment + for (uint256 i = 0; i < $LOG_N - 1; ++i) { + bool dummy_round = i >= ($LOG_N - 1); + + if (!dummy_round) { + // Update inverted denominators + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); + + // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] + mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; + mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; + scalars[boundary + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); + + // Accumulate the const term contribution given by + // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) + Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; + accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; + mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; + } + // Update the running power of v + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; + + commitments[boundary + i] = proof.geminiFoldComms[i]; + } + + boundary += $LOG_N - 1; + + // Finalize the batch opening claim + mem.denominators[0] = Fr.wrap(1).div(tp.shplonkZ - tp.geminiR); + mem.denominators[1] = Fr.wrap(1).div(tp.shplonkZ - SUBGROUP_GENERATOR * tp.geminiR); + mem.denominators[2] = mem.denominators[0]; + mem.denominators[3] = mem.denominators[0]; + + for (uint256 i = 0; i < LIBRA_EVALUATIONS; i++) { + Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; + mem.batchingScalars[i] = scalingFactor.neg(); + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; + mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i]; + } + scalars[boundary] = mem.batchingScalars[0]; + scalars[boundary + 1] = mem.batchingScalars[1] + mem.batchingScalars[2]; + scalars[boundary + 2] = mem.batchingScalars[3]; + + for (uint256 i = 0; i < LIBRA_COMMITMENTS; i++) { + commitments[boundary++] = proof.libraCommitments[i]; + } + + commitments[boundary] = Honk.G1Point({x: 1, y: 2}); + scalars[boundary++] = mem.constantTermAccumulator; + + require( + checkEvalsConsistency(proof.libraPolyEvals, tp.geminiR, tp.sumCheckUChallenges, proof.libraEvaluation), + Errors.ConsistencyCheckFailed() + ); + + Honk.G1Point memory quotient_commitment = proof.kzgQuotient; + + commitments[boundary] = quotient_commitment; + scalars[boundary] = tp.shplonkZ; // evaluation challenge + + PairingInputs memory pair; + pair.P_0 = batchMul(commitments, scalars); + pair.P_1 = negateInplace(quotient_commitment); + + // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) + if (!arePairingPointsDefault(proof.pairingPointObject)) { + Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, pair.P_0, pair.P_1); + (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = + convertPairingPointsToG1(proof.pairingPointObject); + + // Validate the points from the proof are on the curve + rejectPointAtInfinity(P_0_other); + rejectPointAtInfinity(P_1_other); + + // accumulate with aggregate points in proof + pair.P_0 = mulWithSeperator(pair.P_0, P_0_other, recursionSeparator); + pair.P_1 = mulWithSeperator(pair.P_1, P_1_other, recursionSeparator); + } + + return pairing(pair.P_0, pair.P_1); + } + + function checkEvalsConsistency( + Fr[LIBRA_EVALUATIONS] memory libraPolyEvals, + Fr geminiR, + Fr[CONST_PROOF_SIZE_LOG_N] memory uChallenges, + Fr libraEval + ) internal view returns (bool check) { + Fr one = Fr.wrap(1); + Fr vanishingPolyEval = geminiR.pow(SUBGROUP_SIZE) - one; + require(vanishingPolyEval != Fr.wrap(0), Errors.GeminiChallengeInSubgroup()); + + SmallSubgroupIpaIntermediates memory mem; + mem.challengePolyLagrange[0] = one; + for (uint256 round = 0; round < $LOG_N; round++) { + uint256 currIdx = 1 + LIBRA_UNIVARIATES_LENGTH * round; + mem.challengePolyLagrange[currIdx] = one; + for (uint256 idx = currIdx + 1; idx < currIdx + LIBRA_UNIVARIATES_LENGTH; idx++) { + mem.challengePolyLagrange[idx] = mem.challengePolyLagrange[idx - 1] * uChallenges[round]; + } + } + + mem.rootPower = one; + mem.challengePolyEval = Fr.wrap(0); + for (uint256 idx = 0; idx < SUBGROUP_SIZE; idx++) { + mem.denominators[idx] = mem.rootPower * geminiR - one; + mem.denominators[idx] = mem.denominators[idx].invert(); + mem.challengePolyEval = mem.challengePolyEval + mem.challengePolyLagrange[idx] * mem.denominators[idx]; + mem.rootPower = mem.rootPower * SUBGROUP_GENERATOR_INVERSE; + } + + Fr numerator = vanishingPolyEval * Fr.wrap(SUBGROUP_SIZE).invert(); + mem.challengePolyEval = mem.challengePolyEval * numerator; + mem.lagrangeFirst = mem.denominators[0] * numerator; + mem.lagrangeLast = mem.denominators[SUBGROUP_SIZE - 1] * numerator; + + mem.diff = mem.lagrangeFirst * libraPolyEvals[2]; + + mem.diff = mem.diff + (geminiR - SUBGROUP_GENERATOR_INVERSE) + * (libraPolyEvals[1] - libraPolyEvals[2] - libraPolyEvals[0] * mem.challengePolyEval); + mem.diff = mem.diff + mem.lagrangeLast * (libraPolyEvals[2] - libraEval) - vanishingPolyEval * libraPolyEvals[3]; + + check = mem.diff == Fr.wrap(0); + } + + // This implementation is the same as above with different constants + function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) + internal + view + returns (Honk.G1Point memory result) + { + uint256 limit = $MSMSize; + + // Validate all points are on the curve + for (uint256 i = 0; i < limit; ++i) { + rejectPointAtInfinity(base[i]); + } + + bool success = true; + assembly { + let free := mload(0x40) + + let count := 0x01 + for {} lt(count, add(limit, 1)) { count := add(count, 1) } { + // Get loop offsets + let base_base := add(base, mul(count, 0x20)) + let scalar_base := add(scalars, mul(count, 0x20)) + + mstore(add(free, 0x40), mload(mload(base_base))) + mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) + // Add scalar + mstore(add(free, 0x80), mload(scalar_base)) + + success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) + // accumulator = accumulator + accumulator_2 + success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) + } + + // Return the result + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + } + + require(success, Errors.ShpleminiFailed()); + } + + // Calculate proof size based on log_n (matching UltraKeccakZKFlavor formula) + function calculateProofSize(uint256 logN) internal pure returns (uint256) { + // Witness and Libra commitments + uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments + proofLength += NUM_ELEMENTS_COMM * 3; // Libra concat, grand sum, quotient comms + Gemini masking + + // Sumcheck + proofLength += logN * ZK_BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates + proofLength += NUMBER_OF_ENTITIES_ZK * NUM_ELEMENTS_FR; // sumcheck evaluations + + // Libra and Gemini + proofLength += NUM_ELEMENTS_FR * 2; // Libra sum, claimed eval + proofLength += logN * NUM_ELEMENTS_FR; // Gemini a evaluations + proofLength += NUM_LIBRA_EVALUATIONS * NUM_ELEMENTS_FR; // libra evaluations + + // PCS commitments + proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments + proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments + + // Pairing points + proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs + + return proofLength * 32; + } + + function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); +} + +contract HonkVerifier is BaseZKHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { + function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { + return HonkVerificationKey.loadVerificationKey(); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/WithdrawVerifier.sol b/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/WithdrawVerifier.sol new file mode 100644 index 0000000..0f9d5fe --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/contracts/src/verifiers/WithdrawVerifier.sol @@ -0,0 +1,2460 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Aztec +pragma solidity >=0.8.21; + +uint256 constant N = 1048576; +uint256 constant LOG_N = 20; +uint256 constant NUMBER_OF_PUBLIC_INPUTS = 17; +uint256 constant VK_HASH = 0x1ab5db66b188ef0fde6cc3d4830a3f3e6da09e294afafcd6834b84481e2e7b13; +library HonkVerificationKey { + function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { + Honk.VerificationKey memory vk = Honk.VerificationKey({ + circuitSize: uint256(1048576), + logCircuitSize: uint256(20), + publicInputsSize: uint256(17), + ql: Honk.G1Point({ + x: uint256(0x15f73d39e2fd1a733828efd8adf0470a8ab181fa9e9209e3c4b63cf481da4b0e), + y: uint256(0x0fff756b1f2839666f1c23a0c10a898dda4459389ba0202b996029bb9fe68d2f) + }), + qr: Honk.G1Point({ + x: uint256(0x2334f7517ab1e73f79e0fd0021621a10e091580a29c7eced94ccf794af5c5584), + y: uint256(0x14f8edce3bb1f695a2e7d5050f65c2482db8326b82c7ddf5cc2e76a7ff338cf1) + }), + qo: Honk.G1Point({ + x: uint256(0x167a67551dd24012f3ebc5b1feba7a20d3bbc108ea9175302142544bdd6c44e5), + y: uint256(0x2979ee009fde86ad698297c74fddfa15c838da202964ba9d46a56e5efd84359d) + }), + q4: Honk.G1Point({ + x: uint256(0x04f2f731615bb4f17f68758e47104e46d1089ff6e1d5bacfd0228e8ac91c9e22), + y: uint256(0x08b98f6e567cb5fb077c837f31d71834405e16e796578a9bb00c1dd35db5c098) + }), + qm: Honk.G1Point({ + x: uint256(0x16861f7d65c4ef07b4a350de60c0689c36de28e2c7abb9c1ca8f7be7cac05828), + y: uint256(0x080551c6310e269cb5889f481f9d43b87e3e1b60356113084fbd561e6164d64a) + }), + qc: Honk.G1Point({ + x: uint256(0x01bd5b6a872663053424868ffcce72ba6516657bfd3025de6d7c6bbb6fea314c), + y: uint256(0x0aefe4210b80a255031ba19c79aff45dbfd6b6ce3055334cd7ea9e9a7416a111) + }), + qLookup: Honk.G1Point({ + x: uint256(0x0c4032c3079594eb75a8449d3d5ce8bc3661650d53f9b24d923d8f404cb0bbc9), + y: uint256(0x1084d709650356d40f0158fd6da81f54eb5fe796a0ca89441369b7c24301f851) + }), + qArith: Honk.G1Point({ + x: uint256(0x24d1a79a6bdd8612fa1207b25d44118bda8a1ec9f73b2df4b5e12170e40bc8c8), + y: uint256(0x005b958467e30c2ed80db94b56867d22fbf789b5834445b833a84ef190770a1d) + }), + qDeltaRange: Honk.G1Point({ + x: uint256(0x293ddfcc7c9971a4eda88f38c0a7ad0967d2b2d1417819e9c8d461c25162cd57), + y: uint256(0x25d11a7e3963fe82f256e8fc3e3570e89672f22cceed42df12f6492e9973e4aa) + }), + qElliptic: Honk.G1Point({ + x: uint256(0x12209affe6a45708a4aa783c7b260a727a5c50e3cf94d33d4e9baa5a088428cc), + y: uint256(0x1f707b52bcd766e98f7adf43682cfcb435388bd7b4bc44660994500730ee76d0) + }), + qMemory: Honk.G1Point({ + x: uint256(0x056f93a2331b38ec0a02b5fcc1b7a2e3e3762706d7c060984391b7c5ef5300e3), + y: uint256(0x0950555fb73522c3a569ae6aa0b71c204a671a7e4f091de34815b5c7e5d2ca71) + }), + qNnf: Honk.G1Point({ + x: uint256(0x0687afc6353811c1b407c15f037a86f08bad301ffa290787f6af3f44120eb97d), + y: uint256(0x1c08b8deeea0aee04d53fad92cd7647aa8c68496dabd69cc4e4f878e82f21a13) + }), + qPoseidon2External: Honk.G1Point({ + x: uint256(0x00bd572035c55a326a37a9c9c6d07a546735d5c848286619460267daf81406fd), + y: uint256(0x0fc8e8378c3ae072d675ffa653dc006583941eb466f69d6429eee86e31f1be73) + }), + qPoseidon2Internal: Honk.G1Point({ + x: uint256(0x07b844097451e8b44b61027c89b8be0f594722788a5c3b919abc6c4ce3872c3a), + y: uint256(0x124d3e68d97c8d8b05d2418d78e505c25cb586cfa112caeeea2407ae83b07ea1) + }), + s1: Honk.G1Point({ + x: uint256(0x005f65d346bda0cf831362f4421baf51f67cb02602df6611e6c04d3f32ebccf2), + y: uint256(0x113f9198346a1a6f3d4f8c40c3b244b377ef39b9d1934e8be4d6665186471578) + }), + s2: Honk.G1Point({ + x: uint256(0x0b26eac8d21a3d3b36fa05ca01675037ce4e832ff90bcfcd8955dc76d997e8fa), + y: uint256(0x0f065b923b8a69e02e36362bd2222c5867ed3e504b72351b89ece0ed3dfbe841) + }), + s3: Honk.G1Point({ + x: uint256(0x25a4a0b7ac8bff485b77f26a2d361f0303bdb12a93c6ab5ae83df440d3e601cc), + y: uint256(0x05d1456cb89e48fde18250dab2b9724abd962d1a34d329df223b42e85380df29) + }), + s4: Honk.G1Point({ + x: uint256(0x0f9834095be3aa9c55ffb26b53462099252e04038d5513f6a66d6c47c941ea3f), + y: uint256(0x1fe2be45854e460fbb02e1b01a1ffe874d94cda7c43027dcc6a989879458022c) + }), + t1: Honk.G1Point({ + x: uint256(0x099e3bd5a0a00ab7fe18040105b9b395b5d8b7b4a63b05df652b0d10ef146d26), + y: uint256(0x0015b8d2515d76e2ccec99dcd194592129af3a637f5a622a32440f860d1e2a7f) + }), + t2: Honk.G1Point({ + x: uint256(0x1b917517920bad3d8bc01c9595092a222b888108dc25d1aa450e0b4bc212c37e), + y: uint256(0x305e8992b148eedb22e6e992077a84482141c7ebe42000a1d58ccb74381f6d19) + }), + t3: Honk.G1Point({ + x: uint256(0x16465a5ccbb550cd2c63bd58116fe47c86847618681dc29d8a9363ab7c40e1c3), + y: uint256(0x2e24d420fbf9508ed31de692db477b439973ac12d7ca796d6fe98ca40e6ca6b7) + }), + t4: Honk.G1Point({ + x: uint256(0x043d063b130adfb37342af45d0155a28edd1a7e46c840d9c943fdf45521c64ce), + y: uint256(0x261522c4089330646aff96736194949330952ae74c573d1686d9cb4a00733854) + }), + id1: Honk.G1Point({ + x: uint256(0x23912f235810001f016cfc307818db17798dfd63c1c552eacdeea1741bdd6e1d), + y: uint256(0x0b36de66bac5b5d21cf9dc117699a0ba7e4b679754607091420d83151089ca01) + }), + id2: Honk.G1Point({ + x: uint256(0x27f1e84054a0ae31f5cd67b77c4b7098cb56a547e2bfeed606bc40fc8d272122), + y: uint256(0x249d9e0f20df079d090730dd708c5b1bd23da31ff6685df567cc7cf047c88b7a) + }), + id3: Honk.G1Point({ + x: uint256(0x1f038ce1f9b75cf3d25ecb6e4b7d08bdd57e71570eb9f078179394bac33efcf1), + y: uint256(0x2871e77b88fcfcd143931dccce2c4b3d277df370951f82c3dcfe1d4805662e9b) + }), + id4: Honk.G1Point({ + x: uint256(0x06498ddd770cfde841e2a087a466133841e482a77c4e8c5525c901eb64b95f08), + y: uint256(0x075638edf4064e5a3a82a420a402581f60392917ad97a68d0f50bb7f0f197e65) + }), + lagrangeFirst: Honk.G1Point({ + x: uint256(0x0000000000000000000000000000000000000000000000000000000000000001), + y: uint256(0x0000000000000000000000000000000000000000000000000000000000000002) + }), + lagrangeLast: Honk.G1Point({ + x: uint256(0x2a84d7b64ed7cae23dfc2f4baf24912262b406378210849c89a78dbabd210a1d), + y: uint256(0x070a1fdfb6ea42c11695fc2c3265b66ea5e190a018e30cbc00bee0b19086e1af) + }) + }); + return vk; + } +} + +pragma solidity ^0.8.27; + +interface IVerifier { + function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); +} + +/** + * @notice Library of error codes + * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier + */ +library Errors { + error ValueGeLimbMax(); + error ValueGeGroupOrder(); + error ValueGeFieldOrder(); + + error InvertOfZero(); + error NotPowerOfTwo(); + error ModExpFailed(); + + error ProofLengthWrong(); + error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); + error PublicInputsLengthWrong(); + error SumcheckFailed(); + error ShpleminiFailed(); + + error PointAtInfinity(); + + error ConsistencyCheckFailed(); + error GeminiChallengeInSubgroup(); +} + +type Fr is uint256; + +using {add as +} for Fr global; +using {sub as -} for Fr global; +using {mul as *} for Fr global; + +using {notEqual as !=} for Fr global; +using {equal as ==} for Fr global; + +uint256 constant SUBGROUP_SIZE = 256; +uint256 constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Prime field order +uint256 constant P = MODULUS; +Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); +Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); +Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); +Fr constant ONE = Fr.wrap(1); +Fr constant ZERO = Fr.wrap(0); +// Instantiation + +library FrLib { + bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; + + function invert(Fr value) internal view returns (Fr) { + uint256 v = Fr.unwrap(value); + require(v != 0, Errors.InvertOfZero()); + + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), v) + mstore(add(free, 0x80), sub(MODULUS, 2)) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function pow(Fr base, uint256 v) internal view returns (Fr) { + uint256 b = Fr.unwrap(base); + // Only works for power of 2 + require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); + uint256 result; + + // Call the modexp precompile to invert in the field + assembly { + let free := mload(0x40) + mstore(free, 0x20) + mstore(add(free, 0x20), 0x20) + mstore(add(free, 0x40), 0x20) + mstore(add(free, 0x60), b) + mstore(add(free, 0x80), v) + mstore(add(free, 0xa0), MODULUS) + let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) + if iszero(success) { + mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) + revert(0, 0x04) + } + result := mload(0x00) + mstore(0x40, add(free, 0xc0)) + } + + return Fr.wrap(result); + } + + function div(Fr numerator, Fr denominator) internal view returns (Fr) { + unchecked { + return numerator * invert(denominator); + } + } + + function sqr(Fr value) internal pure returns (Fr) { + unchecked { + return value * value; + } + } + + function unwrap(Fr value) internal pure returns (uint256) { + unchecked { + return Fr.unwrap(value); + } + } + + function neg(Fr value) internal pure returns (Fr) { + unchecked { + return Fr.wrap(MODULUS - Fr.unwrap(value)); + } + } + + function from(uint256 value) internal pure returns (Fr) { + unchecked { + require(value < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(value); + } + } + + function fromBytes32(bytes32 value) internal pure returns (Fr) { + unchecked { + uint256 v = uint256(value); + require(v < MODULUS, Errors.ValueGeFieldOrder()); + return Fr.wrap(v); + } + } + + function toBytes32(Fr value) internal pure returns (bytes32) { + unchecked { + return bytes32(Fr.unwrap(value)); + } + } +} + +// Free functions +function add(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function mul(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); + } +} + +function sub(Fr a, Fr b) pure returns (Fr) { + unchecked { + return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); + } +} + +function notEqual(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) != Fr.unwrap(b); + } +} + +function equal(Fr a, Fr b) pure returns (bool) { + unchecked { + return Fr.unwrap(a) == Fr.unwrap(b); + } +} + +uint256 constant CONST_PROOF_SIZE_LOG_N = 25; + +uint256 constant NUMBER_OF_SUBRELATIONS = 28; +uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; +uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; +uint256 constant NUMBER_OF_ENTITIES = 41; +// The number of entities added for ZK (gemini_masking_poly) +uint256 constant NUM_MASKING_POLYNOMIALS = 1; +uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_UNSHIFTED = 36; +uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; +uint256 constant NUMBER_TO_BE_SHIFTED = 5; +uint256 constant PAIRING_POINTS_SIZE = 8; + +uint256 constant FIELD_ELEMENT_SIZE = 0x20; +uint256 constant GROUP_ELEMENT_SIZE = 0x40; + +// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) +uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; + +// ENUM FOR WIRES +enum WIRE { + Q_M, + Q_C, + Q_L, + Q_R, + Q_O, + Q_4, + Q_LOOKUP, + Q_ARITH, + Q_RANGE, + Q_ELLIPTIC, + Q_MEMORY, + Q_NNF, + Q_POSEIDON2_EXTERNAL, + Q_POSEIDON2_INTERNAL, + SIGMA_1, + SIGMA_2, + SIGMA_3, + SIGMA_4, + ID_1, + ID_2, + ID_3, + ID_4, + TABLE_1, + TABLE_2, + TABLE_3, + TABLE_4, + LAGRANGE_FIRST, + LAGRANGE_LAST, + W_L, + W_R, + W_O, + W_4, + Z_PERM, + LOOKUP_INVERSES, + LOOKUP_READ_COUNTS, + LOOKUP_READ_TAGS, + W_L_SHIFT, + W_R_SHIFT, + W_O_SHIFT, + W_4_SHIFT, + Z_PERM_SHIFT +} + +library Honk { + struct G1Point { + uint256 x; + uint256 y; + } + + struct VerificationKey { + // Misc Params + uint256 circuitSize; + uint256 logCircuitSize; + uint256 publicInputsSize; + // Selectors + G1Point qm; + G1Point qc; + G1Point ql; + G1Point qr; + G1Point qo; + G1Point q4; + G1Point qLookup; // Lookup + G1Point qArith; // Arithmetic widget + G1Point qDeltaRange; // Delta Range sort + G1Point qMemory; // Memory + G1Point qNnf; // Non-native Field + G1Point qElliptic; // Auxillary + G1Point qPoseidon2External; + G1Point qPoseidon2Internal; + // Copy constraints + G1Point s1; + G1Point s2; + G1Point s3; + G1Point s4; + // Copy identity + G1Point id1; + G1Point id2; + G1Point id3; + G1Point id4; + // Precomputed lookup table + G1Point t1; + G1Point t2; + G1Point t3; + G1Point t4; + // Fixed first and last + G1Point lagrangeFirst; + G1Point lagrangeLast; + } + + struct RelationParameters { + // challenges + Fr eta; + Fr beta; + Fr gamma; + // derived + Fr publicInputsDelta; + } + + struct Proof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // Free wires + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Lookup helpers - Permutations + G1Point zPerm; + // Lookup helpers - logup + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Sumcheck + Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + G1Point shplonkQ; + G1Point kzgQuotient; + } + + /// forge-lint: disable-next-item(pascal-case-struct) + struct ZKProof { + // Pairing point object + Fr[PAIRING_POINTS_SIZE] pairingPointObject; + // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) + G1Point geminiMaskingPoly; + // Commitments to wire polynomials + G1Point w1; + G1Point w2; + G1Point w3; + G1Point w4; + // Commitments to logup witness polynomials + G1Point lookupReadCounts; + G1Point lookupReadTags; + G1Point lookupInverses; + // Commitment to grand permutation polynomial + G1Point zPerm; + G1Point[3] libraCommitments; + // Sumcheck + Fr libraSum; + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; + Fr libraEvaluation; + Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) + // Shplemini + G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; + Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; + Fr[4] libraPolyEvals; + G1Point shplonkQ; + G1Point kzgQuotient; + } +} + +// ZKTranscript library to generate fiat shamir challenges, the ZK transcript only differest +/// forge-lint: disable-next-item(pascal-case-struct) +struct ZKTranscript { + // Oink + Honk.RelationParameters relationParameters; + Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] + Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; + // Sumcheck + Fr libraChallenge; + Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; + // Shplemini + Fr rho; + Fr geminiR; + Fr shplonkNu; + Fr shplonkZ; + // Derived + Fr publicInputsDelta; +} + +library ZKTranscriptLib { + function generateTranscript( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + uint256 logN + ) external pure returns (ZKTranscript memory t) { + Fr previousChallenge; + (t.relationParameters, previousChallenge) = + generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); + + (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); + + (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); + (t.libraChallenge, previousChallenge) = generateLibraChallenge(previousChallenge, proof); + (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); + + (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); + + (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); + + (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); + + (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); + return t; + } + + function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { + uint256 challengeU256 = uint256(Fr.unwrap(challenge)); + // Split into two equal 127-bit chunks (254/2) + uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits + uint256 hi = challengeU256 >> 127; + first = FrLib.from(lo); + second = FrLib.from(hi); + } + + function generateRelationParametersChallenges( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize, + Fr previousChallenge + ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { + (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); + + (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); + } + + function generateEtaChallenge( + Honk.ZKProof memory proof, + bytes32[] calldata publicInputs, + uint256 vkHash, + uint256 publicInputsSize + ) internal pure returns (Fr eta, Fr previousChallenge) { + // Size: 1 (vkHash) + publicInputsSize + 8 (geminiMask(2) + 3 wires(6)) + bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 8); + round0[0] = bytes32(vkHash); + + for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { + require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); + round0[1 + i] = publicInputs[i]; + } + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); + } + + // For ZK flavors: hash the gemini masking poly commitment (sent right after public inputs) + round0[1 + publicInputsSize] = bytes32(proof.geminiMaskingPoly.x); + round0[1 + publicInputsSize + 1] = bytes32(proof.geminiMaskingPoly.y); + + // Create the first challenge + // Note: w4 is added to the challenge later on + round0[1 + publicInputsSize + 2] = bytes32(proof.w1.x); + round0[1 + publicInputsSize + 3] = bytes32(proof.w1.y); + round0[1 + publicInputsSize + 4] = bytes32(proof.w2.x); + round0[1 + publicInputsSize + 5] = bytes32(proof.w2.y); + round0[1 + publicInputsSize + 6] = bytes32(proof.w3.x); + round0[1 + publicInputsSize + 7] = bytes32(proof.w3.y); + + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); + (eta,) = splitChallenge(previousChallenge); + } + + function generateBetaGammaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) + { + bytes32[7] memory round1; + round1[0] = FrLib.toBytes32(previousChallenge); + round1[1] = bytes32(proof.lookupReadCounts.x); + round1[2] = bytes32(proof.lookupReadCounts.y); + round1[3] = bytes32(proof.lookupReadTags.x); + round1[4] = bytes32(proof.lookupReadTags.y); + round1[5] = bytes32(proof.w4.x); + round1[6] = bytes32(proof.w4.y); + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); + (beta, gamma) = splitChallenge(nextPreviousChallenge); + } + + // Alpha challenges non-linearise the gate contributions + function generateAlphaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) + { + // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup + uint256[5] memory alpha0; + alpha0[0] = Fr.unwrap(previousChallenge); + alpha0[1] = proof.lookupInverses.x; + alpha0[2] = proof.lookupInverses.y; + alpha0[3] = proof.zPerm.x; + alpha0[4] = proof.zPerm.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); + Fr alpha; + (alpha,) = splitChallenge(nextPreviousChallenge); + + // Compute powers of alpha for batching subrelations + alphas[0] = alpha; + for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { + alphas[i] = alphas[i - 1] * alpha; + } + } + + function generateGateChallenges(Fr previousChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) + { + previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); + (gateChallenges[0],) = splitChallenge(previousChallenge); + for (uint256 i = 1; i < logN; i++) { + gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; + } + nextPreviousChallenge = previousChallenge; + } + + function generateLibraChallenge(Fr previousChallenge, Honk.ZKProof memory proof) + internal + pure + returns (Fr libraChallenge, Fr nextPreviousChallenge) + { + // 2 comm, 1 sum, 1 challenge + uint256[4] memory challengeData; + challengeData[0] = Fr.unwrap(previousChallenge); + challengeData[1] = proof.libraCommitments[0].x; + challengeData[2] = proof.libraCommitments[0].y; + challengeData[3] = Fr.unwrap(proof.libraSum); + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(challengeData))) % P); + (libraChallenge,) = splitChallenge(nextPreviousChallenge); + } + + function generateSumcheckChallenges(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) + { + for (uint256 i = 0; i < logN; i++) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; + univariateChal[0] = prevChallenge; + + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; + } + prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); + + (sumcheckChallenges[i],) = splitChallenge(prevChallenge); + } + nextPreviousChallenge = prevChallenge; + } + + // We add Libra claimed eval + 2 libra commitments (grand_sum, quotient) + function generateRhoChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr rho, Fr nextPreviousChallenge) + { + uint256[NUMBER_OF_ENTITIES_ZK + 6] memory rhoChallengeElements; + rhoChallengeElements[0] = Fr.unwrap(prevChallenge); + uint256 i; + for (i = 1; i <= NUMBER_OF_ENTITIES_ZK; i++) { + rhoChallengeElements[i] = Fr.unwrap(proof.sumcheckEvaluations[i - 1]); + } + rhoChallengeElements[i] = Fr.unwrap(proof.libraEvaluation); + i += 1; + rhoChallengeElements[i] = proof.libraCommitments[1].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[1].y; + i += 2; + rhoChallengeElements[i] = proof.libraCommitments[2].x; + rhoChallengeElements[i + 1] = proof.libraCommitments[2].y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); + (rho,) = splitChallenge(nextPreviousChallenge); + } + + function generateGeminiRChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr geminiR, Fr nextPreviousChallenge) + { + uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); + gR[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 0; i < logN - 1; i++) { + gR[1 + i * 2] = proof.geminiFoldComms[i].x; + gR[2 + i * 2] = proof.geminiFoldComms[i].y; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); + + (geminiR,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkNuChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) + internal + pure + returns (Fr shplonkNu, Fr nextPreviousChallenge) + { + uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1 + 4); + shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); + + for (uint256 i = 1; i <= logN; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.geminiAEvaluations[i - 1]); + } + + uint256 libraIdx = 0; + for (uint256 i = logN + 1; i <= logN + 4; i++) { + shplonkNuChallengeElements[i] = Fr.unwrap(proof.libraPolyEvals[libraIdx]); + libraIdx++; + } + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); + (shplonkNu,) = splitChallenge(nextPreviousChallenge); + } + + function generateShplonkZChallenge(Honk.ZKProof memory proof, Fr prevChallenge) + internal + pure + returns (Fr shplonkZ, Fr nextPreviousChallenge) + { + uint256[3] memory shplonkZChallengeElements; + shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); + + shplonkZChallengeElements[1] = proof.shplonkQ.x; + shplonkZChallengeElements[2] = proof.shplonkQ.y; + + nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); + (shplonkZ,) = splitChallenge(nextPreviousChallenge); + } + + function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.ZKProof memory p) { + uint256 boundary = 0x0; + + // Pairing point object + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); + // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 + require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); + p.pairingPointObject[i] = FrLib.from(limb); + boundary += FIELD_ELEMENT_SIZE; + } + + // Gemini masking polynomial commitment (sent first in ZK flavors, right after pairing points) + p.geminiMaskingPoly = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Commitments + p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Lookup / Permutation Helper Commitments + p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[0] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + p.libraSum = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + // Sumcheck univariates + for (uint256 i = 0; i < logN; i++) { + for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { + p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + } + + // Sumcheck evaluations (includes gemini_masking_poly eval at index 0 for ZK flavors) + for (uint256 i = 0; i < NUMBER_OF_ENTITIES_ZK; i++) { + p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + p.libraEvaluation = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + + p.libraCommitments[1] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + p.libraCommitments[2] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + + // Gemini + // Read gemini fold univariates + for (uint256 i = 0; i < logN - 1; i++) { + p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + } + + // Read gemini a evaluations + for (uint256 i = 0; i < logN; i++) { + p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + for (uint256 i = 0; i < 4; i++) { + p.libraPolyEvals[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); + boundary += FIELD_ELEMENT_SIZE; + } + + // Shplonk + p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + boundary += GROUP_ELEMENT_SIZE; + // KZG + p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); + } +} + +library RelationsLib { + struct EllipticParams { + // Points + Fr x_1; + Fr y_1; + Fr x_2; + Fr y_2; + Fr y_3; + Fr x_3; + // push accumulators into memory + Fr x_double_identity; + } + + // Parameters used within the Memory Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct MemParams { + Fr memory_record_check; + Fr partial_record_check; + Fr next_gate_access_type; + Fr record_delta; + Fr index_delta; + Fr adjacent_values_match_if_adjacent_indices_match; + Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; + Fr access_check; + Fr next_gate_access_type_is_boolean; + Fr ROM_consistency_check_identity; + Fr RAM_consistency_check_identity; + Fr timestamp_delta; + Fr RAM_timestamp_check_identity; + Fr memory_identity; + Fr index_is_monotonically_increasing; + } + + // Parameters used within the Non-Native Field Relation + // A struct is used to work around stack too deep. This relation has alot of variables + struct NnfParams { + Fr limb_subproduct; + Fr non_native_field_gate_1; + Fr non_native_field_gate_2; + Fr non_native_field_gate_3; + Fr limb_accumulator_1; + Fr limb_accumulator_2; + Fr nnf_identity; + } + + struct PoseidonExternalParams { + Fr s1; + Fr s2; + Fr s3; + Fr s4; + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr t0; + Fr t1; + Fr t2; + Fr t3; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr q_pos_by_scaling; + } + + struct PoseidonInternalParams { + Fr u1; + Fr u2; + Fr u3; + Fr u4; + Fr u_sum; + Fr v1; + Fr v2; + Fr v3; + Fr v4; + Fr s1; + Fr q_pos_by_scaling; + } + + Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) + uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; + + // Constants for the Non-native Field relation + Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); + Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); + + function accumulateRelationEvaluations( + Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, + Fr powPartialEval + ) internal pure returns (Fr accumulator) { + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; + + // Accumulate all relations in Ultra Honk - each with varying number of subrelations + accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); + accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); + accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); + accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); + + // batch the subrelations with the precomputed alpha powers to obtain the full honk relation + accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); + } + + /** + * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids + * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code + * editors, and thus is noisy. + */ + function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { + return p[uint256(_wire)]; + } + + /** + * Ultra Arithmetic Relation + * + */ + function accumulateArithmeticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + // Relation 0 + Fr q_arith = wire(p, WIRE.Q_ARITH); + { + Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); + + Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; + accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) + + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); + accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); + accum = accum * q_arith; + accum = accum * domainSep; + evals[0] = accum; + } + + // Relation 1 + { + Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); + accum = accum * (q_arith - Fr.wrap(2)); + accum = accum * (q_arith - ONE); + accum = accum * q_arith; + accum = accum * domainSep; + evals[1] = accum; + } + } + + function accumulatePermutationRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr grand_product_numerator; + Fr grand_product_denominator; + + { + Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; + num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); + num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); + + grand_product_numerator = num; + } + { + Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; + den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); + den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); + + grand_product_denominator = den; + } + + // Contribution 2 + { + Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; + + acc = acc + - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) + * grand_product_denominator); + acc = acc * domainSep; + evals[2] = acc; + } + + // Contribution 3 + { + Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; + evals[3] = acc; + } + } + + function accumulateLogDerivativeLookupRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr table_term; + Fr lookup_term; + + // Calculate the write term (the table accumulation) + // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) + + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); + } + + // Calculate the read term + // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ + { + Fr beta_sqr = rp.beta * rp.beta; + Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); + Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); + Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); + + lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) + + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); + } + + Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; + Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; + + Fr inverse_exists_xor = + wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) + - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); + + // Inverse calculated correctly relation + Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; + accumulatorNone = accumulatorNone * domainSep; + + // Inverse + Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; + + Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); + + Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; + + evals[4] = accumulatorNone; + evals[5] = accumulatorOne; + evals[6] = read_tag_boolean_relation * domainSep; + } + + function accumulateDeltaRangeRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + Fr minus_one = ZERO - ONE; + Fr minus_two = ZERO - Fr.wrap(2); + Fr minus_three = ZERO - Fr.wrap(3); + + // Compute wire differences + Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); + Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); + Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); + Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); + + // Contribution 6 + { + Fr acc = delta_1; + acc = acc * (delta_1 + minus_one); + acc = acc * (delta_1 + minus_two); + acc = acc * (delta_1 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[7] = acc; + } + + // Contribution 7 + { + Fr acc = delta_2; + acc = acc * (delta_2 + minus_one); + acc = acc * (delta_2 + minus_two); + acc = acc * (delta_2 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[8] = acc; + } + + // Contribution 8 + { + Fr acc = delta_3; + acc = acc * (delta_3 + minus_one); + acc = acc * (delta_3 + minus_two); + acc = acc * (delta_3 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[9] = acc; + } + + // Contribution 9 + { + Fr acc = delta_4; + acc = acc * (delta_4 + minus_one); + acc = acc * (delta_4 + minus_two); + acc = acc * (delta_4 + minus_three); + acc = acc * wire(p, WIRE.Q_RANGE); + acc = acc * domainSep; + evals[10] = acc; + } + } + + function accumulateEllipticRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + EllipticParams memory ep; + ep.x_1 = wire(p, WIRE.W_R); + ep.y_1 = wire(p, WIRE.W_O); + + ep.x_2 = wire(p, WIRE.W_L_SHIFT); + ep.y_2 = wire(p, WIRE.W_4_SHIFT); + ep.y_3 = wire(p, WIRE.W_O_SHIFT); + ep.x_3 = wire(p, WIRE.W_R_SHIFT); + + Fr q_sign = wire(p, WIRE.Q_L); + Fr q_is_double = wire(p, WIRE.Q_M); + + // Contribution 10 point addition, x-coordinate check + // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 + Fr x_diff = (ep.x_2 - ep.x_1); + Fr y1_sqr = (ep.y_1 * ep.y_1); + { + // Move to top + Fr partialEval = domainSep; + + Fr y2_sqr = (ep.y_2 * ep.y_2); + Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; + Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); + x_add_identity = x_add_identity * x_diff * x_diff; + x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; + + evals[11] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 11 point addition, x-coordinate check + // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 + { + Fr y1_plus_y3 = ep.y_1 + ep.y_3; + Fr y_diff = ep.y_2 * q_sign - ep.y_1; + Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; + evals[12] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); + } + + // Contribution 10 point doubling, x-coordinate check + // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 + // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 + { + Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; + Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; + y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; + Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); + + // NOTE: pushed into memory (stack >:'( ) + ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; + + Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + evals[11] = evals[11] + acc; + } + + // Contribution 11 point doubling, y-coordinate check + // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 + { + Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; + Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); + evals[12] = evals[12] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; + } + } + + function accumulateMemoryRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Honk.RelationParameters memory rp, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + MemParams memory ap; + + // Compute eta powers locally + Fr eta_two = rp.eta * rp.eta; + Fr eta_three = eta_two * rp.eta; + + /** + * MEMORY + * + * A RAM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) + * * v: `value` of memory cell being accessed + * * a: `access` type of record. read: 0 = read, 1 = write + * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three + * + * A ROM memory record contains a tuple of the following fields: + * * i: `index` of memory cell being accessed + * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) + * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three + * + * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + + * selectors, depending on whether the gate is a RAM read/write or a ROM read + * + * | gate type | i | v2/t | v | a | r | + * | --------- | -- | ----- | -- | -- | -- | + * | ROM | w1 | w2 | w3 | -- | w4 | + * | RAM | w1 | w2 | w3 | qc | w4 | + * + * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on + * `w2` to fix its value) + * + * + */ + + /** + * Memory Record Check + * Partial degree: 1 + * Total degree: 4 + * + * A ROM/ROM access gate can be evaluated with the identity: + * + * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 + * + * For ROM gates, qc = 0 + */ + ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); + ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); + ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); + ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 + ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); + + /** + * Contribution 13 & 14 + * ROM Consistency Check + * Partial degree: 1 + * Total degree: 4 + * + * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of + * records that are sorted. + * + * We apply the following checks for the sorted records: + * + * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 + * 2. index values for adjacent records are monotonically increasing + * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} + * + */ + ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); + ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); + + ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 + + ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 + + evals[14] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + evals[15] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) + * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 + + ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 + + /** + * Contributions 15,16,17 + * RAM Consistency Check + * + * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` + * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. + * This is validated by requiring `access` to be boolean + * + * For two adjacent entries in the sorted list if _both_ + * A) index values match + * B) adjacent access value is 0 (i.e. next gate is a READ) + * then + * C) both values must match. + * The gate boolean check is + * (A && B) => C === !(A && B) || C === !A || !B || C + * + * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized + * with a WRITE operation. + */ + Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 + ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 + + // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta + // deg 1 or 4 + ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); + ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); + ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; + + Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); + ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = + (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 + + // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the + // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't + // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access + // type is correct, to cover this edge case + // deg 2 or 4 + ap.next_gate_access_type_is_boolean = + ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; + + // Putting it all together... + evals[16] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation + * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 + evals[17] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 + evals[18] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 6 + + ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 + + /** + * RAM Timestamp Consistency Check + * + * | w1 | w2 | w3 | w4 | + * | index | timestamp | timestamp_check | -- | + * + * Let delta_index = index_{i + 1} - index_{i} + * + * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i + * Else timestamp_check = 0 + */ + ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); + ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg 3 + + /** + * Complete Contribution 12 + * The complete RAM/ROM memory identity + * Partial degree: + */ + ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 + ap.memory_identity = + ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg 4 + ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 or 6 + ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 + + // (deg 3 or 9) + (deg 4) + (deg 3) + ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 + evals[13] = ap.memory_identity; + } + + function accumulateNnfRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + NnfParams memory ap; + + /** + * Contribution 12 + * Non native field arithmetic gate 2 + * deg 4 + * + * _ _ + * / _ _ _ 14 \ + * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | + * \_ _/ + * + * + */ + ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); + ap.non_native_field_gate_2 = + (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; + ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); + + ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; + ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); + ap.non_native_field_gate_1 = ap.limb_subproduct; + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); + ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); + + ap.non_native_field_gate_3 = ap.limb_subproduct; + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); + ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); + + Fr non_native_field_identity = + ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; + non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); + + // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm + // deg 2 + ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; + ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); + ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); + ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); + + // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm + // deg 2 + ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; + ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); + ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); + ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); + + Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; + limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 + + ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; + ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); + evals[19] = ap.nnf_identity; + } + + function accumulatePoseidonExternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonExternalParams memory ep; + + ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); + ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); + ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); + + ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; + ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; + ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; + ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; + // matrix mul v = M_E * u with 14 additions + ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 + ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 + ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 + // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 + ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 + // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 + ep.v4 = ep.t1 + ep.t1; + ep.v4 = ep.v4 + ep.v4 + ep.t3; + // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 + ep.v2 = ep.t0 + ep.t0; + ep.v2 = ep.v2 + ep.v2 + ep.t2; + // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 + ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 + ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 + + ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; + evals[20] = evals[20] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); + + evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); + + evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); + + evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + function accumulatePoseidonInternalRelation( + Fr[NUMBER_OF_ENTITIES] memory p, + Fr[NUMBER_OF_SUBRELATIONS] memory evals, + Fr domainSep + ) internal pure { + PoseidonInternalParams memory ip; + + Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ + FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), + FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), + FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), + FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) + ]; + + // add round constants + ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); + + // apply s-box round + ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; + ip.u2 = wire(p, WIRE.W_R); + ip.u3 = wire(p, WIRE.W_O); + ip.u4 = wire(p, WIRE.W_4); + + // matrix mul with v = M_I * u 4 muls and 7 additions + ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; + + ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; + + ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; + evals[24] = evals[24] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); + + ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; + evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); + + ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; + evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); + + ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; + evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); + } + + // Batch subrelation evaluations using precomputed powers of alpha + // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array + function scaleAndBatchSubrelations( + Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, + Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges + ) internal pure returns (Fr accumulator) { + accumulator = evaluations[0]; + + for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { + accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; + } + } +} + +library CommitmentSchemeLib { + using FrLib for Fr; + + // Avoid stack too deep + struct ShpleminiIntermediates { + Fr unshiftedScalar; + Fr shiftedScalar; + Fr unshiftedScalarNeg; + Fr shiftedScalarNeg; + // Scalar to be multiplied by [1]₁ + Fr constantTermAccumulator; + // Accumulator for powers of rho + Fr batchingChallenge; + // Linear combination of multilinear (sumcheck) evaluations and powers of rho + Fr batchedEvaluation; + Fr[4] denominators; + Fr[4] batchingScalars; + // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr posInvertedDenominator; + // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated + Fr negInvertedDenominator; + // ν^{2i} * 1/(z - r^{2^i}) + Fr scalingFactorPos; + // ν^{2i+1} * 1/(z + r^{2^i}) + Fr scalingFactorNeg; + // Fold_i(r^{2^i}) reconstructed by Verifier + Fr[] foldPosEvaluations; + } + + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 + function computeFoldPosEvaluations( + Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, + Fr batchedEvalAccumulator, + Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, + Fr[] memory geminiEvalChallengePowers, + uint256 logSize + ) internal view returns (Fr[] memory) { + Fr[] memory foldPosEvaluations = new Fr[](logSize); + for (uint256 i = logSize; i > 0; --i) { + Fr challengePower = geminiEvalChallengePowers[i - 1]; + Fr u = sumcheckUChallenges[i - 1]; + + Fr batchedEvalRoundAcc = ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] + * (challengePower * (ONE - u) - u)); + // Divide by the denominator + batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); + + batchedEvalAccumulator = batchedEvalRoundAcc; + foldPosEvaluations[i - 1] = batchedEvalRoundAcc; + } + return foldPosEvaluations; + } + + function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { + Fr[] memory squares = new Fr[](logN); + squares[0] = r; + for (uint256 i = 1; i < logN; ++i) { + squares[i] = squares[i - 1].sqr(); + } + return squares; + } +} + +uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // EC group order. F_q + +// Fr utility + +function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { + scalar = FrLib.fromBytes32(bytes32(proofSection)); +} + +// EC Point utilities +function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { + uint256 x = uint256(bytes32(proofSection[0x00:0x20])); + uint256 y = uint256(bytes32(proofSection[0x20:0x40])); + require(x < Q && y < Q, Errors.ValueGeGroupOrder()); + + // Reject the point at infinity (0,0). EVM precompiles silently treat (0,0) + // as the identity element, which could zero out commitments. + // On-curve validation (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles + // per EIP-196, so we only need to catch this special case here. + require((x | y) != 0, Errors.PointAtInfinity()); + + point = Honk.G1Point({x: x, y: y}); +} + +function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { + // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. + if (point.y != 0) { + point.y = Q - point.y; + } + return point; +} + +/** + * Convert the pairing points to G1 points. + * + * The pairing points are serialised as an array of 2 limbs representing two points + * (P0 and P1, used for lhs and rhs of pairing operation). + * + * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. + * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] + * + * @param pairingPoints The pairing points to convert. + * @return lhs P0 point + * @return rhs P1 point + */ +function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) + pure + returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) +{ + // P0 (lhs): x = lo | (hi << 136) + uint256 lhsX = Fr.unwrap(pairingPoints[0]); + lhsX |= Fr.unwrap(pairingPoints[1]) << 136; + + uint256 lhsY = Fr.unwrap(pairingPoints[2]); + lhsY |= Fr.unwrap(pairingPoints[3]) << 136; + + // P1 (rhs): x = lo | (hi << 136) + uint256 rhsX = Fr.unwrap(pairingPoints[4]); + rhsX |= Fr.unwrap(pairingPoints[5]) << 136; + + uint256 rhsY = Fr.unwrap(pairingPoints[6]); + rhsY |= Fr.unwrap(pairingPoints[7]) << 136; + + // Reconstructed coordinates must be < Q to prevent malleability. + // Without this, two different limb encodings could map to the same curve point + // (via mulmod reduction in on-curve checks) but produce different transcript hashes. + require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); + + lhs.x = lhsX; + lhs.y = lhsY; + rhs.x = rhsX; + rhs.y = rhsY; +} + +/** + * Hash the pairing inputs from the present verification context with those extracted from the public inputs. + * + * @param proofPairingPoints Pairing points from the proof - (public inputs). + * @param accLhs Accumulator point for the left side - result of shplemini. + * @param accRhs Accumulator point for the right side - result of shplemini. + * @return recursionSeparator The recursion separator - generated from hashing the above. + */ +function generateRecursionSeparator( + Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, + Honk.G1Point memory accLhs, + Honk.G1Point memory accRhs +) pure returns (Fr recursionSeparator) { + // hash the proof aggregated X + // hash the proof aggregated Y + // hash the accum X + // hash the accum Y + + (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); + + uint256[8] memory recursionSeparatorElements; + + // Proof points + recursionSeparatorElements[0] = proofLhs.x; + recursionSeparatorElements[1] = proofLhs.y; + recursionSeparatorElements[2] = proofRhs.x; + recursionSeparatorElements[3] = proofRhs.y; + + // Accumulator points + recursionSeparatorElements[4] = accLhs.x; + recursionSeparatorElements[5] = accLhs.y; + recursionSeparatorElements[6] = accRhs.x; + recursionSeparatorElements[7] = accRhs.y; + + recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); +} + +/** + * G1 Mul with Separator + * Using the ecAdd and ecMul precompiles + * + * @param basePoint The point to multiply. + * @param other The other point to add. + * @param recursionSeperator The separator to use for the multiplication. + * @return `(recursionSeperator * basePoint) + other`. + */ +function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) + view + returns (Honk.G1Point memory) +{ + Honk.G1Point memory result; + + result = ecMul(recursionSeperator, basePoint); + result = ecAdd(result, other); + + return result; +} + +/** + * G1 Mul + * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. + * + * @param value The value to multiply the point by. + * @param point The point to multiply. + * @return result The result of the multiplication. + */ +function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write the point into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | point.x + // free + 0x20| point.y + mstore(free, mload(point)) + mstore(add(free, 0x20), mload(add(point, 0x20))) + // Write the scalar into memory (one 32 byte word) + // Memory layout: + // Address | value + // free + 0x40| value + mstore(add(free, 0x40), value) + + // Call the ecMul precompile, it takes in the following + // [point.x, point.y, scalar], and returns the result back into the free memory location. + let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) + if iszero(success) { + revert(0, 0) + } + // Copy the result of the multiplication back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x60)) + } + + return result; +} + +/** + * G1 Add + * Takes two G1 points and uses the ecAdd precompile to return the result. + * + * @param lhs The left hand side of the addition. + * @param rhs The right hand side of the addition. + * @return result The result of the addition. + */ +function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { + Honk.G1Point memory result; + + assembly { + let free := mload(0x40) + // Write lhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free | lhs.x + // free + 0x20| lhs.y + mstore(free, mload(lhs)) + mstore(add(free, 0x20), mload(add(lhs, 0x20))) + + // Write rhs into memory (two 32 byte words) + // Memory layout: + // Address | value + // free + 0x40| rhs.x + // free + 0x60| rhs.y + mstore(add(free, 0x40), mload(rhs)) + mstore(add(free, 0x60), mload(add(rhs, 0x20))) + + // Call the ecAdd precompile, it takes in the following + // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. + let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) + if iszero(success) { revert(0, 0) } + + // Copy the result of the addition back into the result memory location. + // Memory layout: + // Address | value + // result | result.x + // result + 0x20| result.y + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + + mstore(0x40, add(free, 0x80)) + } + + return result; +} + +function rejectPointAtInfinity(Honk.G1Point memory point) pure { + require((point.x | point.y) != 0, Errors.PointAtInfinity()); +} + +/** + * Check if pairing point limbs are all zero (default/infinity). + * Default pairing points indicate no recursive verification occurred. + */ +function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { + uint256 acc = 0; + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + acc |= Fr.unwrap(pairingPoints[i]); + } + return acc == 0; +} + +function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { + bytes memory input = abi.encodePacked( + rhs.x, + rhs.y, + // Fixed G2 point + uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), + uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), + uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), + uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), + lhs.x, + lhs.y, + // G2 point from VK + uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), + uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), + uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), + uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) + ); + + (bool success, bytes memory result) = address(0x08).staticcall(input); + decodedResult = success && abi.decode(result, (bool)); +} + +abstract contract BaseZKHonkVerifier is IVerifier { + using FrLib for Fr; + + struct PairingInputs { + Honk.G1Point P_0; + Honk.G1Point P_1; + } + + struct SmallSubgroupIpaIntermediates { + Fr[SUBGROUP_SIZE] challengePolyLagrange; + Fr challengePolyEval; + Fr lagrangeFirst; + Fr lagrangeLast; + Fr rootPower; + Fr[SUBGROUP_SIZE] denominators; // this has to disappear + Fr diff; + } + + // Constants for proof length calculation (matching UltraKeccakZKFlavor) + uint256 internal constant NUM_WITNESS_ENTITIES = 8 + NUM_MASKING_POLYNOMIALS; + uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points + uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements + uint256 internal constant NUM_LIBRA_EVALUATIONS = 4; // libra evaluations + + uint256 internal constant LIBRA_COMMITMENTS = 3; + uint256 internal constant LIBRA_EVALUATIONS = 4; + uint256 internal constant LIBRA_UNIVARIATES_LENGTH = 9; + + uint256 internal constant SHIFTED_COMMITMENTS_START = 30; + uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; + + uint256 internal immutable $N; + uint256 internal immutable $LOG_N; + uint256 internal immutable $VK_HASH; + uint256 internal immutable $NUM_PUBLIC_INPUTS; + uint256 internal immutable $MSMSize; + + constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { + $N = _N; + $LOG_N = _logN; + $VK_HASH = _vkHash; + $NUM_PUBLIC_INPUTS = _numPublicInputs; + $MSMSize = NUMBER_UNSHIFTED_ZK + _logN + LIBRA_COMMITMENTS + 2; + } + + function verify(bytes calldata proof, bytes32[] calldata publicInputs) + public + view + override + returns (bool verified) + { + // Calculate expected proof size based on $LOG_N + uint256 expectedProofSize = calculateProofSize($LOG_N); + + // Check the received proof is the expected size where each field element is 32 bytes + require( + proof.length == expectedProofSize, Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize) + ); + + Honk.VerificationKey memory vk = loadVerificationKey(); + Honk.ZKProof memory p = ZKTranscriptLib.loadProof(proof, $LOG_N); + + require(publicInputs.length == vk.publicInputsSize - PAIRING_POINTS_SIZE, Errors.PublicInputsLengthWrong()); + + // Generate the fiat shamir challenges for the whole protocol + ZKTranscript memory t = + ZKTranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); + + // Derive public input delta + t.relationParameters.publicInputsDelta = computePublicInputDelta( + publicInputs, + p.pairingPointObject, + t.relationParameters.beta, + t.relationParameters.gamma, /*pubInputsOffset=*/ + 1 + ); + + // Sumcheck + require(verifySumcheck(p, t), Errors.SumcheckFailed()); + require(verifyShplemini(p, vk, t), Errors.ShpleminiFailed()); + + verified = true; + } + + function computePublicInputDelta( + bytes32[] memory publicInputs, + Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, + Fr beta, + Fr gamma, + uint256 offset + ) internal view returns (Fr publicInputDelta) { + Fr numerator = Fr.wrap(1); + Fr denominator = Fr.wrap(1); + + Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); + Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); + + { + for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { + Fr pubInput = FrLib.fromBytes32(publicInputs[i]); + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + + for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { + Fr pubInput = pairingPointObject[i]; + + numerator = numerator * (numeratorAcc + pubInput); + denominator = denominator * (denominatorAcc + pubInput); + + numeratorAcc = numeratorAcc + beta; + denominatorAcc = denominatorAcc - beta; + } + } + + // Fr delta = numerator / denominator; // TOOO: batch invert later? + publicInputDelta = FrLib.div(numerator, denominator); + } + + function verifySumcheck(Honk.ZKProof memory proof, ZKTranscript memory tp) internal view returns (bool verified) { + Fr roundTargetSum = tp.libraChallenge * proof.libraSum; // default 0 + Fr powPartialEvaluation = Fr.wrap(1); + + // We perform sumcheck reductions over log n rounds ( the multivariate degree ) + for (uint256 round; round < $LOG_N; ++round) { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; + Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; + require(totalSum == roundTargetSum, Errors.SumcheckFailed()); + + Fr roundChallenge = tp.sumCheckUChallenges[round]; + + // Update the round target for the next rounf + roundTargetSum = computeNextTargetSum(roundUnivariate, roundChallenge); + powPartialEvaluation = + powPartialEvaluation * (Fr.wrap(1) + roundChallenge * (tp.gateChallenges[round] - Fr.wrap(1))); + } + + // Last round + // For ZK flavors: sumcheckEvaluations has 42 elements + // Index 0 is gemini_masking_poly, indices 1-41 are the regular entities used in relations + Fr[NUMBER_OF_ENTITIES] memory relationsEvaluations; + for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { + relationsEvaluations[i] = proof.sumcheckEvaluations[i + NUM_MASKING_POLYNOMIALS]; // Skip gemini_masking_poly at index 0 + } + Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( + relationsEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation + ); + + Fr evaluation = Fr.wrap(1); + for (uint256 i = 2; i < $LOG_N; i++) { + evaluation = evaluation * tp.sumCheckUChallenges[i]; + } + + grandHonkRelationSum = + grandHonkRelationSum * (Fr.wrap(1) - evaluation) + proof.libraEvaluation * tp.libraChallenge; + verified = (grandHonkRelationSum == roundTargetSum); + } + + // Return the new target sum for the next sumcheck round + function computeNextTargetSum(Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) + internal + view + returns (Fr targetSum) + { + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000240), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), + Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), + Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), + Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80) + ]; + + // To compute the next target sum, we evaluate the given univariate at a point u (challenge). + + // Performing Barycentric evaluations + // Compute B(x) + Fr numeratorValue = Fr.wrap(1); + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); + } + + Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + denominatorInverses[i] = FrLib.invert(BARYCENTRIC_LAGRANGE_DENOMINATORS[i] * (roundChallenge - Fr.wrap(i))); + } + + for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { + targetSum = targetSum + roundUnivariates[i] * denominatorInverses[i]; + } + + // Scale the sum by the value of B(x) + targetSum = targetSum * numeratorValue; + } + + function verifyShplemini(Honk.ZKProof memory proof, Honk.VerificationKey memory vk, ZKTranscript memory tp) + internal + view + returns (bool verified) + { + CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack + + // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size + Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); + // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings + Fr[] memory scalars = new Fr[]($MSMSize); + Honk.G1Point[] memory commitments = new Honk.G1Point[]($MSMSize); + + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); + + mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); + mem.shiftedScalar = + tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); + + scalars[0] = Fr.wrap(1); + commitments[0] = proof.shplonkQ; + + /* Batch multivariate opening claims, shifted and unshifted + * The vector of scalars is populated as follows: + * \f[ + * \left( + * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), + * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), + * \ldots, + * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) + * \right) + * \f] + * + * The following vector is concatenated to the vector of commitments: + * \f[ + * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} + * \f] + * + * Simultaneously, the evaluation of the multilinear polynomial + * \f[ + * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} + * \f] + * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. + * + * This approach minimizes the number of iterations over the commitments to multilinear polynomials + * and eliminates the need to store the powers of \f$ \rho \f$. + */ + // For ZK flavors: evaluations array is [gemini_masking_poly, qm, qc, ql, qr, ...] + // Start batching challenge at 1, not rho, to match non-ZK pattern + mem.batchingChallenge = Fr.wrap(1); + mem.batchedEvaluation = Fr.wrap(0); + + mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); + mem.shiftedScalarNeg = mem.shiftedScalar.neg(); + + // Process all NUMBER_UNSHIFTED_ZK evaluations (includes gemini_masking_poly at index 0) + for (uint256 i = 1; i <= NUMBER_UNSHIFTED_ZK; ++i) { + scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; + mem.batchedEvaluation = mem.batchedEvaluation + + (proof.sumcheckEvaluations[i - NUM_MASKING_POLYNOMIALS] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + // g commitments are accumulated at r + // For each of the to be shifted commitments perform the shift in place by + // adding to the unshifted value. + // We do so, as the values are to be used in batchMul later, and as + // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. + // Applied to w1, w2, w3, w4 and zPerm + for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { + uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; + uint256 evaluationOff = i + NUMBER_UNSHIFTED_ZK; + + scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); + mem.batchedEvaluation = + mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); + mem.batchingChallenge = mem.batchingChallenge * tp.rho; + } + + commitments[1] = proof.geminiMaskingPoly; + + commitments[2] = vk.qm; + commitments[3] = vk.qc; + commitments[4] = vk.ql; + commitments[5] = vk.qr; + commitments[6] = vk.qo; + commitments[7] = vk.q4; + commitments[8] = vk.qLookup; + commitments[9] = vk.qArith; + commitments[10] = vk.qDeltaRange; + commitments[11] = vk.qElliptic; + commitments[12] = vk.qMemory; + commitments[13] = vk.qNnf; + commitments[14] = vk.qPoseidon2External; + commitments[15] = vk.qPoseidon2Internal; + commitments[16] = vk.s1; + commitments[17] = vk.s2; + commitments[18] = vk.s3; + commitments[19] = vk.s4; + commitments[20] = vk.id1; + commitments[21] = vk.id2; + commitments[22] = vk.id3; + commitments[23] = vk.id4; + commitments[24] = vk.t1; + commitments[25] = vk.t2; + commitments[26] = vk.t3; + commitments[27] = vk.t4; + commitments[28] = vk.lagrangeFirst; + commitments[29] = vk.lagrangeLast; + + // Accumulate proof points + commitments[30] = proof.w1; + commitments[31] = proof.w2; + commitments[32] = proof.w3; + commitments[33] = proof.w4; + commitments[34] = proof.zPerm; + commitments[35] = proof.lookupInverses; + commitments[36] = proof.lookupReadCounts; + commitments[37] = proof.lookupReadTags; + + /* Batch gemini claims from the prover + * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from + * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars + * + * 1. Moves the vector + * \f[ + * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) + * \f] + * to the 'commitments' vector. + * + * 2. Computes the scalars: + * \f[ + * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} + * \f] + * and places them into the 'scalars' vector. + * + * 3. Accumulates the summands of the constant term: + * \f[ + * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} + * \f] + * and adds them to the 'constant_term_accumulator'. + */ + + // Add contributions from A₀(r) and A₀(-r) to constant_term_accumulator: + // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 + Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( + tp.sumCheckUChallenges, + mem.batchedEvaluation, + proof.geminiAEvaluations, + powers_of_evaluation_challenge, + $LOG_N + ); + + mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; + mem.constantTermAccumulator = + mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); + + mem.batchingChallenge = tp.shplonkNu.sqr(); + uint256 boundary = NUMBER_UNSHIFTED_ZK + 1; + + // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; + // Compute scalar multipliers for each fold commitment + for (uint256 i = 0; i < $LOG_N - 1; ++i) { + bool dummy_round = i >= ($LOG_N - 1); + + if (!dummy_round) { + // Update inverted denominators + mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); + mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); + + // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] + mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; + mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; + scalars[boundary + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); + + // Accumulate the const term contribution given by + // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) + Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; + accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; + mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; + } + // Update the running power of v + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; + + commitments[boundary + i] = proof.geminiFoldComms[i]; + } + + boundary += $LOG_N - 1; + + // Finalize the batch opening claim + mem.denominators[0] = Fr.wrap(1).div(tp.shplonkZ - tp.geminiR); + mem.denominators[1] = Fr.wrap(1).div(tp.shplonkZ - SUBGROUP_GENERATOR * tp.geminiR); + mem.denominators[2] = mem.denominators[0]; + mem.denominators[3] = mem.denominators[0]; + + for (uint256 i = 0; i < LIBRA_EVALUATIONS; i++) { + Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; + mem.batchingScalars[i] = scalingFactor.neg(); + mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; + mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i]; + } + scalars[boundary] = mem.batchingScalars[0]; + scalars[boundary + 1] = mem.batchingScalars[1] + mem.batchingScalars[2]; + scalars[boundary + 2] = mem.batchingScalars[3]; + + for (uint256 i = 0; i < LIBRA_COMMITMENTS; i++) { + commitments[boundary++] = proof.libraCommitments[i]; + } + + commitments[boundary] = Honk.G1Point({x: 1, y: 2}); + scalars[boundary++] = mem.constantTermAccumulator; + + require( + checkEvalsConsistency(proof.libraPolyEvals, tp.geminiR, tp.sumCheckUChallenges, proof.libraEvaluation), + Errors.ConsistencyCheckFailed() + ); + + Honk.G1Point memory quotient_commitment = proof.kzgQuotient; + + commitments[boundary] = quotient_commitment; + scalars[boundary] = tp.shplonkZ; // evaluation challenge + + PairingInputs memory pair; + pair.P_0 = batchMul(commitments, scalars); + pair.P_1 = negateInplace(quotient_commitment); + + // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) + if (!arePairingPointsDefault(proof.pairingPointObject)) { + Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, pair.P_0, pair.P_1); + (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = + convertPairingPointsToG1(proof.pairingPointObject); + + // Validate the points from the proof are on the curve + rejectPointAtInfinity(P_0_other); + rejectPointAtInfinity(P_1_other); + + // accumulate with aggregate points in proof + pair.P_0 = mulWithSeperator(pair.P_0, P_0_other, recursionSeparator); + pair.P_1 = mulWithSeperator(pair.P_1, P_1_other, recursionSeparator); + } + + return pairing(pair.P_0, pair.P_1); + } + + function checkEvalsConsistency( + Fr[LIBRA_EVALUATIONS] memory libraPolyEvals, + Fr geminiR, + Fr[CONST_PROOF_SIZE_LOG_N] memory uChallenges, + Fr libraEval + ) internal view returns (bool check) { + Fr one = Fr.wrap(1); + Fr vanishingPolyEval = geminiR.pow(SUBGROUP_SIZE) - one; + require(vanishingPolyEval != Fr.wrap(0), Errors.GeminiChallengeInSubgroup()); + + SmallSubgroupIpaIntermediates memory mem; + mem.challengePolyLagrange[0] = one; + for (uint256 round = 0; round < $LOG_N; round++) { + uint256 currIdx = 1 + LIBRA_UNIVARIATES_LENGTH * round; + mem.challengePolyLagrange[currIdx] = one; + for (uint256 idx = currIdx + 1; idx < currIdx + LIBRA_UNIVARIATES_LENGTH; idx++) { + mem.challengePolyLagrange[idx] = mem.challengePolyLagrange[idx - 1] * uChallenges[round]; + } + } + + mem.rootPower = one; + mem.challengePolyEval = Fr.wrap(0); + for (uint256 idx = 0; idx < SUBGROUP_SIZE; idx++) { + mem.denominators[idx] = mem.rootPower * geminiR - one; + mem.denominators[idx] = mem.denominators[idx].invert(); + mem.challengePolyEval = mem.challengePolyEval + mem.challengePolyLagrange[idx] * mem.denominators[idx]; + mem.rootPower = mem.rootPower * SUBGROUP_GENERATOR_INVERSE; + } + + Fr numerator = vanishingPolyEval * Fr.wrap(SUBGROUP_SIZE).invert(); + mem.challengePolyEval = mem.challengePolyEval * numerator; + mem.lagrangeFirst = mem.denominators[0] * numerator; + mem.lagrangeLast = mem.denominators[SUBGROUP_SIZE - 1] * numerator; + + mem.diff = mem.lagrangeFirst * libraPolyEvals[2]; + + mem.diff = mem.diff + (geminiR - SUBGROUP_GENERATOR_INVERSE) + * (libraPolyEvals[1] - libraPolyEvals[2] - libraPolyEvals[0] * mem.challengePolyEval); + mem.diff = mem.diff + mem.lagrangeLast * (libraPolyEvals[2] - libraEval) - vanishingPolyEval * libraPolyEvals[3]; + + check = mem.diff == Fr.wrap(0); + } + + // This implementation is the same as above with different constants + function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) + internal + view + returns (Honk.G1Point memory result) + { + uint256 limit = $MSMSize; + + // Validate all points are on the curve + for (uint256 i = 0; i < limit; ++i) { + rejectPointAtInfinity(base[i]); + } + + bool success = true; + assembly { + let free := mload(0x40) + + let count := 0x01 + for {} lt(count, add(limit, 1)) { count := add(count, 1) } { + // Get loop offsets + let base_base := add(base, mul(count, 0x20)) + let scalar_base := add(scalars, mul(count, 0x20)) + + mstore(add(free, 0x40), mload(mload(base_base))) + mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) + // Add scalar + mstore(add(free, 0x80), mload(scalar_base)) + + success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) + // accumulator = accumulator + accumulator_2 + success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) + } + + // Return the result + mstore(result, mload(free)) + mstore(add(result, 0x20), mload(add(free, 0x20))) + } + + require(success, Errors.ShpleminiFailed()); + } + + // Calculate proof size based on log_n (matching UltraKeccakZKFlavor formula) + function calculateProofSize(uint256 logN) internal pure returns (uint256) { + // Witness and Libra commitments + uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments + proofLength += NUM_ELEMENTS_COMM * 3; // Libra concat, grand sum, quotient comms + Gemini masking + + // Sumcheck + proofLength += logN * ZK_BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates + proofLength += NUMBER_OF_ENTITIES_ZK * NUM_ELEMENTS_FR; // sumcheck evaluations + + // Libra and Gemini + proofLength += NUM_ELEMENTS_FR * 2; // Libra sum, claimed eval + proofLength += logN * NUM_ELEMENTS_FR; // Gemini a evaluations + proofLength += NUM_LIBRA_EVALUATIONS * NUM_ELEMENTS_FR; // libra evaluations + + // PCS commitments + proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments + proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments + + // Pairing points + proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs + + return proofLength * 32; + } + + function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); +} + +contract HonkVerifier is BaseZKHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { + function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { + return HonkVerificationKey.loadVerificationKey(); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/contracts/test/ShieldedPoolExt.t.sol b/pocs/private-payment/shielded-pool-extension/contracts/test/ShieldedPoolExt.t.sol new file mode 100644 index 0000000..4ba5b5e --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/contracts/test/ShieldedPoolExt.t.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test, stdStorage, StdStorage} from "forge-std/src/Test.sol"; +import {ShieldedPoolExt} from "../src/ShieldedPoolExt.sol"; +import {MockVerifier} from "../src/mocks/MockVerifier.sol"; +import {MockERC20} from "../src/mocks/MockERC20.sol"; +import {PoseidonT3} from "poseidon-solidity/PoseidonT3.sol"; + +/// @dev Scope: deposit, epoch rollover, and the full spend path — transfer +/// (2-in-2-out) and withdraw (single input). With `MockVerifier` the proofs +/// always accept, so these tests cover the contract's public-input +/// marshaling, cross-proof wiring, and state transitions — not the +/// in-circuit/proof-level checks (commitment membership, sorted-low-leaf +/// double-spend, η binding), which are exercised by the circuit tests and +/// the bb-prover integration harness. +contract ShieldedPoolExtTest is Test { + using stdStorage for StdStorage; + + ShieldedPoolExt public pool; + MockVerifier public verifier; // deposit + MockVerifier public transferVerifier; // wallet transfer spend proof + MockVerifier public insertionVerifier; // relayer 2-insertion proof + MockVerifier public withdrawVerifier; // wallet withdraw spend proof + MockVerifier public withdrawInsertionVerifier; // relayer 1-insertion proof + MockERC20 public token; + + address public owner; + address public user; + address public recipient; + + // Small commitment values that fit in the BN254 scalar field. + bytes32 constant COMMITMENT_0 = bytes32(uint256(1)); + bytes32 constant COMMITMENT_1 = bytes32(uint256(2)); + uint256 constant DEPOSIT_AMOUNT = 1000e6; + + // Stand-in for the off-chain-computed empty indexed-Merkle-tree root. The + // contract treats it opaquely (stores it, resets to it); its true value is + // pinned to the Rust mirror in the integration test. + bytes32 constant EMPTY_IMT_ROOT = bytes32(uint256(0xE3217)); + + // Stand-in for the chain-update circuit's VK hash (SPEC `FixedVK`); the + // contract pins it as the spend proof's `chain_vk_hash` public input. + bytes32 constant CHAIN_VK_HASH = bytes32(uint256(0xC4A14)); + + event Deposit(bytes32 indexed commitment, address indexed token, uint256 amount, bytes encryptedNote); + event Transfer( + bytes32 indexed nullifier1, + bytes32 indexed nullifier2, + bytes32 commitment1, + bytes32 commitment2, + bytes encryptedNotes + ); + event Withdraw(bytes32 indexed nullifier, address indexed recipient, address indexed token, uint256 amount); + event EpochRollover(uint64 indexed epoch, bytes32 root); + + function setUp() public { + owner = address(this); + user = address(0x1); + recipient = address(0x2); + + verifier = new MockVerifier(); + transferVerifier = new MockVerifier(); + insertionVerifier = new MockVerifier(); + withdrawVerifier = new MockVerifier(); + withdrawInsertionVerifier = new MockVerifier(); + pool = new ShieldedPoolExt( + address(verifier), + address(transferVerifier), + address(insertionVerifier), + address(withdrawVerifier), + address(withdrawInsertionVerifier), + CHAIN_VK_HASH, + EMPTY_IMT_ROOT + ); + + token = new MockERC20("USD Coin", "USDC", 6); + pool.addSupportedToken(address(token)); + + token.mint(user, DEPOSIT_AMOUNT * 10); + vm.prank(user); + token.approve(address(pool), type(uint256).max); + } + + // ========== Deposit ========== + + function testDeposit() public { + bytes memory encryptedNote = "encrypted_data"; + + vm.prank(user); + vm.expectEmit(true, true, false, true); + emit Deposit(COMMITMENT_0, address(token), DEPOSIT_AMOUNT, encryptedNote); + + // The contract passes currentEpoch as the deposit proof's 4th public + // input, enforcing epoch_created == currentEpoch in the verified proof. + pool.deposit("", COMMITMENT_0, address(token), DEPOSIT_AMOUNT, encryptedNote); + + assertEq(pool.getCommitmentCount(), 1); + assertTrue(pool.commitmentRoot() != bytes32(0)); + assertEq(token.balanceOf(address(pool)), DEPOSIT_AMOUNT); + } + + function testDepositRevertsUnsupportedToken() public { + MockERC20 unsupported = new MockERC20("Unsupported", "UNS", 18); + vm.prank(user); + vm.expectRevert(ShieldedPoolExt.UnsupportedToken.selector); + pool.deposit("", COMMITMENT_0, address(unsupported), DEPOSIT_AMOUNT, ""); + } + + function testDepositRevertsZeroAmount() public { + vm.prank(user); + vm.expectRevert(ShieldedPoolExt.ZeroAmount.selector); + pool.deposit("", COMMITMENT_0, address(token), 0, ""); + } + + function testDepositRevertsInvalidProof() public { + verifier.setResult(false); + vm.prank(user); + vm.expectRevert(ShieldedPoolExt.InvalidProof.selector); + pool.deposit("", COMMITMENT_0, address(token), DEPOSIT_AMOUNT, ""); + } + + function testDepositUpdatesRoot() public { + vm.prank(user); + pool.deposit("", COMMITMENT_0, address(token), DEPOSIT_AMOUNT, ""); + bytes32 root1 = pool.commitmentRoot(); + + vm.prank(user); + pool.deposit("", COMMITMENT_1, address(token), DEPOSIT_AMOUNT, ""); + bytes32 root2 = pool.commitmentRoot(); + + assertTrue(root1 != root2); + assertTrue(pool.isKnownRoot(root1)); // historical root retained + } + + // ========== Epoch rollover ========== + + function testInitialActiveState() public view { + assertEq(pool.currentEpoch(), 0); + assertEq(pool.activeLeafCount(), 1, "genesis leaf occupies index 0"); + assertEq(pool.activeNullifierRoot(), EMPTY_IMT_ROOT); + assertEq(pool.emptyImtRoot(), EMPTY_IMT_ROOT); + } + + function testRolloverFreezesCurrentRootAndResets() public { + // Simulate the spend path having advanced the active root. + bytes32 advancedRoot = bytes32(uint256(0xBEEF)); + stdstore.target(address(pool)).sig("activeNullifierRoot()").checked_write(advancedRoot); + assertEq(pool.activeNullifierRoot(), advancedRoot); + + vm.expectEmit(true, false, false, true); + emit EpochRollover(0, advancedRoot); + pool.rolloverEpoch(); + + assertEq(pool.frozenNullifierRoots(0), advancedRoot, "froze the pre-rollover root"); + assertEq(pool.activeNullifierRoot(), EMPTY_IMT_ROOT, "active tree reset"); + assertEq(pool.activeLeafCount(), 1, "leaf count reset to genesis"); + assertEq(pool.currentEpoch(), 1); + } + + function testRolloverIncrementsEpoch() public { + pool.rolloverEpoch(); + assertEq(pool.currentEpoch(), 1); + pool.rolloverEpoch(); + assertEq(pool.currentEpoch(), 2); + + // Each past epoch's root is recorded (empty here, since no spends yet). + assertEq(pool.frozenNullifierRoots(0), EMPTY_IMT_ROOT); + assertEq(pool.frozenNullifierRoots(1), EMPTY_IMT_ROOT); + } + + function testRolloverOnlyOwner() public { + vm.prank(user); + vm.expectRevert(ShieldedPoolExt.OnlyOwner.selector); + pool.rolloverEpoch(); + } + + // ========== Transfer (two-proof spend path) ========== + + bytes32 constant NUL_0 = bytes32(uint256(0xA1)); + bytes32 constant NUL_1 = bytes32(uint256(0xA2)); + bytes32 constant OUT_0 = bytes32(uint256(11)); + bytes32 constant OUT_1 = bytes32(uint256(12)); + bytes32 constant POST_ROOT = bytes32(uint256(0xBEEF)); + + /// @dev Deposit once so the commitment root is non-zero and `isKnownRoot`. + function _depositAndGetRoot() internal returns (bytes32) { + vm.prank(user); + pool.deposit("", COMMITMENT_0, address(token), DEPOSIT_AMOUNT, ""); + return pool.commitmentRoot(); + } + + function _transfer(bytes32 root, bytes32 postRoot) internal { + bytes32[2] memory nullifiers = [NUL_0, NUL_1]; + bytes32[2] memory outs = [OUT_0, OUT_1]; + uint64[2] memory epochCreated = [uint64(0), uint64(0)]; + pool.transfer("spend", "insert", nullifiers, outs, root, epochCreated, postRoot, "notes"); + } + + function testTransferAdvancesActiveTreeAndInsertsOutputs() public { + bytes32 root = _depositAndGetRoot(); + uint64 leafBefore = pool.activeLeafCount(); + uint256 countBefore = pool.getCommitmentCount(); + + vm.expectEmit(true, true, false, true); + emit Transfer(NUL_0, NUL_1, OUT_0, OUT_1, "notes"); + _transfer(root, POST_ROOT); + + assertEq(pool.activeNullifierRoot(), POST_ROOT, "active root advanced to post-root"); + assertEq(pool.activeLeafCount(), leafBefore + 2, "two leaves appended"); + assertEq(pool.getCommitmentCount(), countBefore + 2, "two output commitments inserted"); + } + + function testTransferAcceptsHistoricalRoot() public { + bytes32 firstRoot = _depositAndGetRoot(); + // A second deposit supersedes `firstRoot`, which stays valid as historical. + vm.prank(user); + pool.deposit("", COMMITMENT_1, address(token), DEPOSIT_AMOUNT, ""); + assertTrue(pool.isKnownRoot(firstRoot)); + + _transfer(firstRoot, POST_ROOT); + assertEq(pool.activeNullifierRoot(), POST_ROOT); + } + + function testTransferRevertsUnknownRoot() public { + vm.expectRevert(ShieldedPoolExt.InvalidRoot.selector); + _transfer(bytes32(uint256(0xDEAD)), POST_ROOT); + } + + function testTransferRevertsInvalidSpendProof() public { + bytes32 root = _depositAndGetRoot(); + transferVerifier.setResult(false); + vm.expectRevert(ShieldedPoolExt.InvalidProof.selector); + _transfer(root, POST_ROOT); + } + + function testTransferRevertsInvalidInsertionProof() public { + bytes32 root = _depositAndGetRoot(); + insertionVerifier.setResult(false); + vm.expectRevert(ShieldedPoolExt.InvalidProof.selector); + _transfer(root, POST_ROOT); + } + + // ========== expectedChainAccumulator ========== + + function testExpectedChainAccumulatorZeroWhenCaughtUp() public view { + // A note created in the current epoch has no frozen epochs to fold. + assertEq(pool.expectedChainAccumulator(pool.currentEpoch()), bytes32(0)); + } + + function testExpectedChainAccumulatorFoldsFrozenRoots() public { + pool.rolloverEpoch(); // freezes epoch 0 (EMPTY_IMT_ROOT; no spends) + pool.rolloverEpoch(); // freezes epoch 1 + assertEq(pool.currentEpoch(), 2); + + // Full range [0, 2): fold both frozen roots, seeded at 0. + uint256 acc = PoseidonT3.hash([uint256(0), uint256(EMPTY_IMT_ROOT)]); + acc = PoseidonT3.hash([acc, uint256(EMPTY_IMT_ROOT)]); + assertEq(pool.expectedChainAccumulator(0), bytes32(acc), "fold over both frozen epochs"); + + // Suffix [1, 2): fold only epoch 1. + uint256 accSuffix = PoseidonT3.hash([uint256(0), uint256(EMPTY_IMT_ROOT)]); + assertEq(pool.expectedChainAccumulator(1), bytes32(accSuffix), "fold over the suffix only"); + + // Empty range [2, 2): nothing to fold. + assertEq(pool.expectedChainAccumulator(2), bytes32(0), "fresh note folds nothing"); + } + + // ========== Withdraw (single-input two-proof spend path) ========== + + bytes32 constant WNUL = bytes32(uint256(0xB1)); + + function _withdraw(bytes32 root, uint256 amount) internal { + pool.withdraw("spend", "insert", WNUL, address(token), amount, recipient, root, 0, POST_ROOT); + } + + function testWithdrawAdvancesActiveTreeAndTransfersOut() public { + bytes32 root = _depositAndGetRoot(); // pool now holds DEPOSIT_AMOUNT; root known + uint64 leafBefore = pool.activeLeafCount(); + uint256 recipientBefore = token.balanceOf(recipient); + uint256 poolBefore = token.balanceOf(address(pool)); + + vm.expectEmit(true, true, true, true); + emit Withdraw(WNUL, recipient, address(token), DEPOSIT_AMOUNT); + _withdraw(root, DEPOSIT_AMOUNT); + + assertEq(pool.activeNullifierRoot(), POST_ROOT, "active root advanced to post-root"); + assertEq(pool.activeLeafCount(), leafBefore + 1, "one leaf appended"); + assertEq(token.balanceOf(recipient), recipientBefore + DEPOSIT_AMOUNT, "funds sent to recipient"); + assertEq(token.balanceOf(address(pool)), poolBefore - DEPOSIT_AMOUNT, "funds left the pool"); + } + + function testWithdrawRevertsUnsupportedToken() public { + MockERC20 unsupported = new MockERC20("Unsupported", "UNS", 18); + vm.expectRevert(ShieldedPoolExt.UnsupportedToken.selector); + pool.withdraw("s", "i", WNUL, address(unsupported), 1, recipient, bytes32(0), 0, POST_ROOT); + } + + function testWithdrawRevertsZeroAmount() public { + vm.expectRevert(ShieldedPoolExt.ZeroAmount.selector); + pool.withdraw("s", "i", WNUL, address(token), 0, recipient, bytes32(0), 0, POST_ROOT); + } + + function testWithdrawRevertsZeroRecipient() public { + vm.expectRevert(ShieldedPoolExt.ZeroAddress.selector); + pool.withdraw("s", "i", WNUL, address(token), 1, address(0), bytes32(0), 0, POST_ROOT); + } + + function testWithdrawRevertsUnknownRoot() public { + vm.expectRevert(ShieldedPoolExt.InvalidRoot.selector); + _withdraw(bytes32(uint256(0xDEAD)), 1); + } + + function testWithdrawRevertsInvalidSpendProof() public { + bytes32 root = _depositAndGetRoot(); + withdrawVerifier.setResult(false); + vm.expectRevert(ShieldedPoolExt.InvalidProof.selector); + _withdraw(root, DEPOSIT_AMOUNT); + } + + function testWithdrawRevertsInvalidInsertionProof() public { + bytes32 root = _depositAndGetRoot(); + withdrawInsertionVerifier.setResult(false); + vm.expectRevert(ShieldedPoolExt.InvalidProof.selector); + _withdraw(root, DEPOSIT_AMOUNT); + } + + // ========== Misc ========== + + function testConstructorWiring() public view { + assertEq(address(pool.depositVerifier()), address(verifier)); + assertEq(address(pool.transferVerifier()), address(transferVerifier)); + assertEq(address(pool.insertionVerifier()), address(insertionVerifier)); + assertEq(address(pool.withdrawVerifier()), address(withdrawVerifier)); + assertEq(address(pool.withdrawInsertionVerifier()), address(withdrawInsertionVerifier)); + assertEq(pool.chainVkHash(), CHAIN_VK_HASH); + } + + /// @dev Each verifier slot rejects the zero address; index marks which slot. + function _construct(uint256 zeroAt) internal returns (ShieldedPoolExt) { + address[5] memory v = [ + address(verifier), + address(transferVerifier), + address(insertionVerifier), + address(withdrawVerifier), + address(withdrawInsertionVerifier) + ]; + v[zeroAt] = address(0); + return new ShieldedPoolExt(v[0], v[1], v[2], v[3], v[4], CHAIN_VK_HASH, EMPTY_IMT_ROOT); + } + + function testConstructorRevertsZeroVerifier() public { + for (uint256 i = 0; i < 5; i++) { + vm.expectRevert(ShieldedPoolExt.ZeroAddress.selector); + _construct(i); + } + } + + function testAddSupportedTokenOnlyOwner() public { + vm.prank(user); + vm.expectRevert(ShieldedPoolExt.OnlyOwner.selector); + pool.addSupportedToken(address(0x123)); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/foundry.toml b/pocs/private-payment/shielded-pool-extension/foundry.toml new file mode 100644 index 0000000..db1b220 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/foundry.toml @@ -0,0 +1,31 @@ +[profile.default] +src = "contracts/src" +out = "contracts/out" +libs = ["contracts/dependencies", "dependencies"] +test = "contracts/test" +script = "contracts/script" +cache_path = "contracts/cache" +broadcast = "contracts/broadcast" +# Headroom for the deploy script's single-context simulation, which creates five +# large bb verifiers (deposit/transfer/withdraw + the k=2 and k=1 insertion ones). +gas_limit = 100000000 +remappings = [ + "@openzeppelin-contracts/=dependencies/@openzeppelin-contracts-5.6.0-rc.1/", + "@zk-kit/=dependencies/zk-kit-v1/", + "forge-std/=dependencies/forge-std-1.14.0", + "poseidon-solidity/=dependencies/poseidon-solidity-0.0.5/", +] +fs_permissions = [{ access = "read-write", path = "./" }] +optimizer = true +optimizer_runs = 100 + +[soldeer] +remappings_generate = true +remappings_prefix = "@" +remappings_location = "config" + +[dependencies] +forge-std = "1.14.0" +"@openzeppelin-contracts" = "5.6.0-rc.1" +zk-kit = { version = "v1", git = "https://github.com/zk-kit/zk-kit.solidity.git", rev = "a171c845ec7fdc50cdd1fe96c14c27d707cdfbed" } +poseidon-solidity = "0.0.5" diff --git a/pocs/private-payment/shielded-pool-extension/rust-toolchain.toml b/pocs/private-payment/shielded-pool-extension/rust-toolchain.toml new file mode 100644 index 0000000..e714338 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.89" +components = ["rustfmt", "clippy"] +profile = "default" diff --git a/pocs/private-payment/shielded-pool-extension/scripts/generate-verifiers.sh b/pocs/private-payment/shielded-pool-extension/scripts/generate-verifiers.sh new file mode 100755 index 0000000..43943fb --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/scripts/generate-verifiers.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Generate Solidity verifiers from the EVM-targeted Noir circuits. +# +# Five circuits are verified on-chain: deposit, the two spend circuits (transfer, +# withdraw), and the relayer insertion proof at both leaf counts — `insertion` +# (k=2, a 2-in transfer) and `insertion_withdraw` (k=1, a 1-in withdraw), two thin +# instantiations of the shared `insertion_core` logic. The chain-update circuit is +# consumed recursively off-chain (no Solidity verifier). +# +# VK generation needs only the compiled circuit, not a witness, so there is no +# Prover.toml / `nargo execute` step. Each verifier is emitted as bb's default +# `HonkVerifier`; forge disambiguates the same-named contracts by file path, and +# `ShieldedPoolExt` takes verifier *addresses*, so the contract names need not be +# unique. +# +# Requires nargo 1.0.0-beta.21 + bb 5.0.0-nightly on PATH (see README). +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +TARGET_DIR="$PROJECT_ROOT/target" +VERIFIERS_DIR="$PROJECT_ROOT/contracts/src/verifiers" + +mkdir -p "$VERIFIERS_DIR" + +CIRCUITS=("deposit" "transfer" "withdraw" "insertion" "insertion_withdraw") + +echo "=== Generating Solidity Verifiers ===" +echo "Output directory: $VERIFIERS_DIR" +echo "" + +echo "Compiling circuits..." +(cd "$PROJECT_ROOT" && nargo compile --workspace) +echo "" + +for circuit in "${CIRCUITS[@]}"; do + echo "Processing $circuit circuit..." + + VK_DIR="$TARGET_DIR/vk_${circuit}" + rm -rf "$VK_DIR" + + echo " [1/2] Generating verification key (keccak oracle)..." + bb write_vk -b "$TARGET_DIR/${circuit}.json" -o "$VK_DIR" --oracle_hash keccak + + # Default: capitalize the package name (deposit -> DepositVerifier). The k=1 + # insertion circuit maps to the contract's `withdrawInsertionVerifier` slot. + case "$circuit" in + insertion_withdraw) CONTRACT_NAME="WithdrawInsertionVerifier" ;; + *) CONTRACT_NAME="$(echo ${circuit:0:1} | tr '[:lower:]' '[:upper:]')${circuit:1}Verifier" ;; + esac + OUTPUT_FILE="$VERIFIERS_DIR/${CONTRACT_NAME}.sol" + + echo " [2/2] Generating Solidity verifier: $OUTPUT_FILE" + bb write_solidity_verifier -k "$VK_DIR/vk" -o "$OUTPUT_FILE" + + echo " Done: $OUTPUT_FILE" + echo "" +done + +echo "=== All verifiers generated successfully ===" diff --git a/pocs/private-payment/shielded-pool-extension/soldeer.lock b/pocs/private-payment/shielded-pool-extension/soldeer.lock new file mode 100644 index 0000000..2b62fbc --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/soldeer.lock @@ -0,0 +1,26 @@ +[[dependencies]] +name = "@openzeppelin-contracts" +version = "5.6.0-rc.1" +url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/5_6_0-rc_1_12-01-2026_23:01:57_contracts.zip" +checksum = "d37187263f82783c5eaeeb3c18d0147e23d54ae33f5966fce2a7bac60ce81480" +integrity = "964d6bbaf7ea075943d73945a3263e4d098ca7597978a296be48cbe1a792eb85" + +[[dependencies]] +name = "forge-std" +version = "1.14.0" +url = "https://soldeer-revisions.s3.amazonaws.com/forge-std/1_14_0_05-01-2026_15:24:39_forge-std-1.14.zip" +checksum = "d10079d1b7aac1a36dd2eeb4c5d042e8ed27b00f4ef89530b7f0a6ecd18cada3" +integrity = "64133846def1b04b1726d2bf1292c19050a0e8cb832b60c6104c19595b9699f8" + +[[dependencies]] +name = "poseidon-solidity" +version = "0.0.5" +url = "https://soldeer-revisions.s3.amazonaws.com/poseidon-solidity/0_0_5_04-07-2024_06:57:44_contracts.zip" +checksum = "c622cf4f348014da6947d3ddeba267317f1ca3701466f92e0be64a7726c3b31c" +integrity = "fecc3410994eebc6edf14e86fd388c31d8095c4abbba4a5772e5287f381df2fd" + +[[dependencies]] +name = "zk-kit" +version = "v1" +git = "https://github.com/zk-kit/zk-kit.solidity.git" +rev = "a171c845ec7fdc50cdd1fe96c14c27d707cdfbed" diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/adapters.rs b/pocs/private-payment/shielded-pool-extension/src/lib/adapters.rs new file mode 100644 index 0000000..e44511d --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/adapters.rs @@ -0,0 +1,9 @@ +pub mod bb_prover; +pub mod commitment_pir; +pub mod ethereum_rpc; +pub mod indexed_merkle_tree; +pub mod light_client; +pub mod merkle_tree; +pub mod pir; +pub mod pir_client; +pub mod state_replica; diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/adapters/bb_prover.rs b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/bb_prover.rs new file mode 100644 index 0000000..7d4c2b4 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/bb_prover.rs @@ -0,0 +1,794 @@ +//! Barretenberg prover adapter. +//! +//! Wires the **chain-update** recursion (the IVC inner artifact): +//! `bb ... -t noir-recursive` for recursion-friendly proofs, reading the raw +//! `vk` / `vk_hash` / `proof` / `public_inputs` binaries and chunking them into +//! Fr field-decimals (the segfaulting `proof_as_fields_honk` is avoided). The +//! EVM-targeted spend/insertion proving uses keccak (`-t evm`). +//! +//! Requires `nargo` 1.0.0-beta.21 + `bb` 5.0.0-nightly on PATH (see the +//! `noir-recursion-recipe` memory). The recursion test is `#[ignore]`d (it +//! shells out and proves real circuits — minutes); run with +//! `cargo test --lib -- --ignored bb_prover`. + +use std::{ + path::PathBuf, + process::Command, +}; + +use alloy::primitives::B256; +use ark_bn254::Fr; +use ark_ff::PrimeField; +use serde::Serialize; + +use crate::domain::indexed_merkle::InsertionWitness; + +/// UltraHonk VK length in field elements (bb 5.0-nightly). +pub const ULTRA_VK_LENGTH_IN_FIELDS: usize = 115; +/// UltraHonk ZK recursive-proof length in field elements. +pub const RECURSIVE_PROOF_LENGTH: usize = 458; +/// ChainProof public-input count. +pub const CHAIN_PROOF_PUB_LEN: usize = 5; + +#[derive(Debug, thiserror::Error)] +pub enum BbError { + #[error("io: {0}")] + Io(#[from] std::io::Error), + #[error("nargo {0} failed: {1}")] + Nargo(String, String), + #[error("bb {0} failed: {1}")] + Bb(String, String), + #[error("unexpected artifact size: {0}")] + Size(String), +} + +fn fr_from_be(bytes: &[u8]) -> Fr { + Fr::from_be_bytes_mod_order(bytes) +} + +fn fr_to_decimal(f: Fr) -> String { + f.into_bigint().to_string() +} + +/// Field element (32 bytes, big-endian) as the decimal string Noir's `Prover.toml` +/// expects. +pub fn field_to_decimal(value: B256) -> String { + fr_to_decimal(fr_from_be(&value.0)) +} + +/// Chunk a binary artifact into field-decimals (32 bytes each, big-endian). +fn bytes_to_decimals(bytes: &[u8], expect: usize) -> Result, BbError> { + if bytes.len() != expect * 32 { + return Err(BbError::Size(format!( + "{} bytes; expected {} fields ({} bytes)", + bytes.len(), + expect, + expect * 32 + ))); + } + Ok(bytes + .chunks_exact(32) + .map(|c| fr_to_decimal(fr_from_be(c))) + .collect()) +} + +/// A produced chain-update proof and its public inputs, field-encoded for use as +/// the recursive witness of the next chain-update proof. +#[derive(Debug, Clone)] +pub struct ChainUpdateArtifact { + pub proof: Vec, + pub public_inputs: Vec, +} + +/// `Prover.toml` shape for the chain-update circuit (field names match the +/// circuit's `main` parameters). +#[derive(Serialize)] +pub struct ChainUpdateInput { + pub commitment: String, + pub epoch_created: String, + pub epoch_validated_through: String, + pub accumulator: String, + pub fixed_vk_hash: String, + pub is_base_case: bool, + pub prior_vk: Vec, + pub prior_proof: Vec, + pub prior_public_inputs: Vec, + pub frozen_root_next: String, + pub spending_key: String, + pub token: String, + pub amount: String, + pub salt: String, + pub low_value: String, + pub low_next_value: String, + pub low_next_index: String, + pub path_bits: Vec, + pub siblings: Vec, +} + +/// Transfer spend-proof public-input count. +pub const TRANSFER_PUB_LEN: usize = 11; + +/// `Prover.toml` shape for the transfer spend circuit (field names match the +/// circuit's `main` parameters). The two `chain_*` recursion witnesses are the +/// inner chain-update proof + VK produced by [`BbProver::prove_chain_update`] / +/// [`BbProver::write_chain_update_vk`]; a zero/padding input note leaves its +/// `chain_proof_i` / `chain_pub_i` zeroed (the circuit skips its recursive verify). +#[derive(Serialize)] +pub struct TransferInput { + pub nullifier_active_0: String, + pub nullifier_active_1: String, + pub commitment_out_0: String, + pub commitment_out_1: String, + pub commitment_root: String, + pub current_epoch: String, + pub chain_vk_hash: String, + pub epoch_created_in_0: String, + pub epoch_created_in_1: String, + pub chain_accumulator_in_0: String, + pub chain_accumulator_in_1: String, + pub spending_key: String, + pub token_in_0: String, + pub amount_in_0: u64, + pub salt_in_0: String, + pub token_in_1: String, + pub amount_in_1: u64, + pub salt_in_1: String, + pub token_out_0: String, + pub amount_out_0: u64, + pub owner_out_0: String, + pub salt_out_0: String, + pub token_out_1: String, + pub amount_out_1: u64, + pub owner_out_1: String, + pub salt_out_1: String, + pub proof_length: u32, + pub path_0: Vec, + pub indices_0: Vec, + pub path_1: Vec, + pub indices_1: Vec, + pub chain_vk: Vec, + pub chain_proof_0: Vec, + pub chain_pub_0: Vec, + pub chain_proof_1: Vec, + pub chain_pub_1: Vec, +} + +/// `Prover.toml` shape for the withdraw circuit (field names match its `main`): +/// a single input note fully exiting to `recipient` (no change outputs), so one +/// chain proof and one nullifier. +#[derive(Serialize)] +pub struct WithdrawInput { + pub nullifier_active: String, + pub token: String, + pub amount: u64, + pub recipient: String, + pub commitment_root: String, + pub current_epoch: String, + pub chain_vk_hash: String, + pub epoch_created_in: String, + pub chain_accumulator_in: String, + pub spending_key: String, + pub salt: String, + pub proof_length: u32, + pub path: Vec, + pub indices: Vec, + pub chain_vk: Vec, + pub chain_proof: Vec, + pub chain_pub: Vec, +} + +/// A produced EVM-target proof and its public inputs, as raw bytes (the form the +/// Solidity `HonkVerifier` consumes: `proof` and the `bytes32[]` public inputs). +#[derive(Debug, Clone)] +pub struct EvmProof { + pub proof: Vec, + pub public_inputs: Vec, +} + +/// `Prover.toml` shape for the deposit circuit (field names match its `main`). +#[derive(Serialize)] +pub struct DepositInput { + pub commitment: String, + pub token: String, + pub amount: String, + pub current_epoch_at_deposit: u64, + pub owner_pubkey: String, + pub salt: String, +} + +/// `Prover.toml` shape for the insertion circuit (k = N_INSERTS). Build it from a +/// relayer insertion via [`InsertionToml::from_witnesses`]. +#[derive(Serialize)] +pub struct InsertionToml { + pub pre_active_root: String, + pub post_active_root: String, + pub pre_leaf_count: u32, + pub nullifiers: Vec, + pub low_value: Vec, + pub low_next_value: Vec, + pub low_next_index: Vec, + pub low_path_bits: Vec>, + pub low_siblings: Vec>, + pub new_siblings: Vec>, +} + +impl InsertionToml { + /// Map a relayer insertion (pre/post roots, leaf count, the ordered nullifier + /// list, and the per-insertion [`InsertionWitness`]es) to the insertion + /// circuit's `Prover.toml`. The circuit derives each new-leaf slot from + /// `pre_leaf_count + i`, so only the predecessor index is supplied — as its + /// little-endian path bits. + pub fn from_witnesses( + pre_active_root: B256, + post_active_root: B256, + pre_leaf_count: u64, + nullifiers: &[B256], + witnesses: &[InsertionWitness], + ) -> Self { + let bits = |index: u64| (0..32).map(|i| (index >> i) & 1 == 1).collect::>(); + let fields = |xs: &[B256]| xs.iter().map(|x| field_to_decimal(*x)).collect::>(); + Self { + pre_active_root: field_to_decimal(pre_active_root), + post_active_root: field_to_decimal(post_active_root), + pre_leaf_count: pre_leaf_count as u32, + nullifiers: nullifiers.iter().map(|n| field_to_decimal(*n)).collect(), + low_value: witnesses.iter().map(|w| field_to_decimal(w.low_leaf.value)).collect(), + low_next_value: witnesses.iter().map(|w| field_to_decimal(w.low_leaf.next_value)).collect(), + low_next_index: witnesses.iter().map(|w| w.low_leaf.next_index.to_string()).collect(), + low_path_bits: witnesses.iter().map(|w| bits(w.low_leaf_index)).collect(), + low_siblings: witnesses.iter().map(|w| fields(&w.low_leaf_siblings)).collect(), + new_siblings: witnesses.iter().map(|w| fields(&w.new_leaf_siblings)).collect(), + } + } +} + +/// Shells `nargo` + `bb` to produce chain-update (recursive) proofs. +pub struct BbProver { + /// Extension root (the Noir workspace root; `target/` and `circuits/` live here). + project_root: PathBuf, +} + +impl BbProver { + pub fn new(project_root: PathBuf) -> Self { + Self { project_root } + } + + fn circuit_json(&self) -> PathBuf { + self.project_root.join("target").join("chain_update.json") + } + + fn workspace_witness(&self) -> PathBuf { + self.project_root.join("target").join("witness.gz") + } + + fn circuit_dir(&self) -> PathBuf { + self.project_root.join("circuits").join("chain_update") + } + + /// Per-circuit dir holding `vk` / `vk_hash` / `proof` / `public_inputs`. + fn artifact_dir(&self) -> PathBuf { + self.circuit_dir().join("target") + } + + /// Compile + write the chain-update VK (recursion target). Returns + /// `(vk_fields[115], vk_hash_decimal)`. `vk_hash` is the circuit's + /// `fixed_vk_hash` (the value the spend circuit pins). + pub fn write_chain_update_vk(&self) -> Result<(Vec, String), BbError> { + let compile = Command::new("nargo") + .args(["compile", "--package", "chain_update"]) + .current_dir(&self.project_root) + .output()?; + if !compile.status.success() { + return Err(BbError::Nargo( + "compile".into(), + String::from_utf8_lossy(&compile.stderr).into(), + )); + } + + std::fs::create_dir_all(self.artifact_dir())?; + let out = Command::new("bb") + .args([ + "write_vk", + "-b", + self.circuit_json().to_str().unwrap(), + "-o", + self.artifact_dir().to_str().unwrap(), + "-t", + "noir-recursive", + ]) + .output()?; + if !out.status.success() { + return Err(BbError::Bb( + "write_vk".into(), + String::from_utf8_lossy(&out.stderr).into(), + )); + } + + let vk = bytes_to_decimals( + &std::fs::read(self.artifact_dir().join("vk"))?, + ULTRA_VK_LENGTH_IN_FIELDS, + )?; + let vk_hash_bytes = std::fs::read(self.artifact_dir().join("vk_hash"))?; + let vk_hash = fr_to_decimal(fr_from_be(&vk_hash_bytes)); + Ok((vk, vk_hash)) + } + + /// Prove the chain-update circuit for the given inputs. Returns the recursive + /// proof + its public inputs. The recursive verify of any prior proof runs + /// during `nargo execute`, so a successful return is itself proof that the + /// prior chain proof recursively verified. + pub fn prove_chain_update( + &self, + input: &ChainUpdateInput, + ) -> Result { + let toml = toml::to_string(input).expect("serialize chain-update input"); + std::fs::write(self.circuit_dir().join("Prover.toml"), toml)?; + + let exec = Command::new("nargo") + .args(["execute", "witness"]) + .current_dir(self.circuit_dir()) + .output()?; + if !exec.status.success() { + return Err(BbError::Nargo( + "execute".into(), + String::from_utf8_lossy(&exec.stderr).into(), + )); + } + + let prove = Command::new("bb") + .args([ + "prove", + "-b", + self.circuit_json().to_str().unwrap(), + "-w", + self.workspace_witness().to_str().unwrap(), + "-k", + self.artifact_dir().join("vk").to_str().unwrap(), + "-o", + self.artifact_dir().to_str().unwrap(), + "-t", + "noir-recursive", + ]) + .output()?; + if !prove.status.success() { + return Err(BbError::Bb( + "prove".into(), + String::from_utf8_lossy(&prove.stderr).into(), + )); + } + + let proof = bytes_to_decimals( + &std::fs::read(self.artifact_dir().join("proof"))?, + RECURSIVE_PROOF_LENGTH, + )?; + let public_inputs = bytes_to_decimals( + &std::fs::read(self.artifact_dir().join("public_inputs"))?, + CHAIN_PROOF_PUB_LEN, + )?; + Ok(ChainUpdateArtifact { + proof, + public_inputs, + }) + } + + fn evm_circuit_json(&self, package: &str) -> PathBuf { + self.project_root.join("target").join(format!("{package}.json")) + } + + fn evm_circuit_dir(&self, package: &str) -> PathBuf { + self.project_root.join("circuits").join(package) + } + + fn evm_artifact_dir(&self, package: &str) -> PathBuf { + self.evm_circuit_dir(package).join("target") + } + + /// Compile `package` and write its EVM-target (keccak) VK — the same VK + /// `scripts/generate-verifiers.sh` turns into the Solidity verifier. + pub fn write_evm_vk(&self, package: &str) -> Result<(), BbError> { + let compile = Command::new("nargo") + .args(["compile", "--package", package]) + .current_dir(&self.project_root) + .output()?; + if !compile.status.success() { + return Err(BbError::Nargo( + "compile".into(), + String::from_utf8_lossy(&compile.stderr).into(), + )); + } + + std::fs::create_dir_all(self.evm_artifact_dir(package))?; + let out = Command::new("bb") + .args([ + "write_vk", + "-b", + self.evm_circuit_json(package).to_str().unwrap(), + "-o", + self.evm_artifact_dir(package).to_str().unwrap(), + "-t", + "evm", + ]) + .output()?; + if !out.status.success() { + return Err(BbError::Bb( + "write_vk".into(), + String::from_utf8_lossy(&out.stderr).into(), + )); + } + Ok(()) + } + + /// Prove `package` (EVM target) from its `Prover.toml`-shaped `input`. Any + /// in-circuit recursive verify (e.g. the transfer circuit's chain-proof + /// verify) runs during `nargo execute`, so a successful return implies it + /// verified. Requires [`Self::write_evm_vk`] for `package` first. + pub fn prove_evm(&self, package: &str, input: &T) -> Result { + let toml = toml::to_string(input).expect("serialize prover input"); + std::fs::write(self.evm_circuit_dir(package).join("Prover.toml"), toml)?; + + let exec = Command::new("nargo") + .args(["execute", "witness"]) + .current_dir(self.evm_circuit_dir(package)) + .output()?; + if !exec.status.success() { + return Err(BbError::Nargo( + "execute".into(), + String::from_utf8_lossy(&exec.stderr).into(), + )); + } + + let prove = Command::new("bb") + .args([ + "prove", + "-b", + self.evm_circuit_json(package).to_str().unwrap(), + "-w", + self.workspace_witness().to_str().unwrap(), + "-k", + self.evm_artifact_dir(package).join("vk").to_str().unwrap(), + "-o", + self.evm_artifact_dir(package).to_str().unwrap(), + "-t", + "evm", + ]) + .output()?; + if !prove.status.success() { + return Err(BbError::Bb( + "prove".into(), + String::from_utf8_lossy(&prove.stderr).into(), + )); + } + + Ok(EvmProof { + proof: std::fs::read(self.evm_artifact_dir(package).join("proof"))?, + public_inputs: std::fs::read(self.evm_artifact_dir(package).join("public_inputs"))?, + }) + } + + /// Verify `package`'s last EVM proof against its VK via `bb verify`, reading + /// the `vk` / `proof` / `public_inputs` left in its artifact dir. + pub fn verify_evm(&self, package: &str) -> Result { + let dir = self.evm_artifact_dir(package); + let out = Command::new("bb") + .args([ + "verify", + "-k", + dir.join("vk").to_str().unwrap(), + "-p", + dir.join("proof").to_str().unwrap(), + "-i", + dir.join("public_inputs").to_str().unwrap(), + "-t", + "evm", + ]) + .output()?; + Ok(out.status.success()) + } + + /// Compile + write the transfer circuit's EVM VK. + pub fn write_transfer_vk(&self) -> Result<(), BbError> { + self.write_evm_vk("transfer") + } + + /// Prove the transfer spend circuit (EVM target; recursively verifies each + /// input's chain proof). See [`Self::prove_evm`]. + pub fn prove_transfer(&self, input: &TransferInput) -> Result { + self.prove_evm("transfer", input) + } + + /// Verify the last transfer proof (EVM target). + pub fn verify_transfer_evm(&self) -> Result { + self.verify_evm("transfer") + } +} + +#[cfg(test)] +mod tests { + use alloy::primitives::{ + Address, + U256, + }; + + use super::*; + use crate::{ + adapters::indexed_merkle_tree::IndexedMerkleTree, + domain::{ + chain_proof::ChainProof, + epoch::Epoch, + indexed_merkle::NonMembershipWitness, + keys::SpendingKey, + note::Note, + }, + }; + + fn project_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + } + + fn zeros(n: usize) -> Vec { + vec!["0".to_string(); n] + } + + // Field-encode the note's commitment-preimage inputs exactly as + // `Note::commitment` does, so the circuit's key-binding check matches. + fn token_field(token: Address) -> String { + field_to_decimal(B256::left_padding_from(token.as_slice())) + } + + fn path_bits_of(index: u64) -> Vec { + (0..32).map(|i| (index >> i) & 1 == 1).collect() + } + + fn siblings_of(w: &NonMembershipWitness) -> Vec { + w.siblings.iter().map(|s| field_to_decimal(*s)).collect() + } + + /// Genesis → step1 → step2 across two (empty) frozen epochs. Each step + /// recursively verifies the prior chain-update proof; success across both + /// steps validates self-recursion + the on-chain-consistent accumulator fold + /// + IMT non-membership + key binding (and the poseidon2/3/1 cross-impl). + #[test] + #[ignore = "shells nargo+bb and proves real circuits (minutes)"] + fn chain_extends_across_two_frozen_epochs() { + let prover = BbProver::new(project_root()); + let (vk_fields, vk_hash) = prover.write_chain_update_vk().expect("write_vk"); + + // Deterministic note. + let sk = SpendingKey::from_bytes([7u8; 32]); + let owner = sk.derive_owner_pubkey(); + let token = Address::repeat_byte(0x11); + let amount = U256::from(100u64); + let salt = B256::repeat_byte(0x05); + let note = Note::with_salt(token, amount, owner, salt, Epoch(0)); + let commitment = note.commitment(); + + // Empty frozen tree (the note was never spent, so its phantom nullifiers + // are absent). Both epochs reuse the empty root for this test. + let frozen = IndexedMerkleTree::new(); + let frozen_root = frozen.root(); + + let note_fields = |i: &mut ChainUpdateInput| { + i.spending_key = field_to_decimal(sk.0); + i.token = token_field(token); + i.amount = field_to_decimal(B256::from(amount)); + i.salt = field_to_decimal(salt); + }; + + // --- Genesis (base case) --- + let genesis = ChainProof::genesis(commitment, Epoch(0)); + let mut g = ChainUpdateInput { + commitment: field_to_decimal(commitment.0), + epoch_created: "0".into(), + epoch_validated_through: "0".into(), + accumulator: "0".into(), + fixed_vk_hash: vk_hash.clone(), + is_base_case: true, + prior_vk: zeros(ULTRA_VK_LENGTH_IN_FIELDS), + prior_proof: zeros(RECURSIVE_PROOF_LENGTH), + prior_public_inputs: zeros(CHAIN_PROOF_PUB_LEN), + frozen_root_next: "0".into(), + spending_key: "0".into(), + token: "0".into(), + amount: "0".into(), + salt: "0".into(), + low_value: "0".into(), + low_next_value: "0".into(), + low_next_index: "0".into(), + path_bits: vec![false; 32], + siblings: zeros(32), + }; + note_fields(&mut g); + let genesis_art = prover.prove_chain_update(&g).expect("prove genesis"); + + // --- Step builder: fold one frozen epoch, recursively verifying `prior`. --- + let step = |prior_state: ChainProof, prior_art: &ChainUpdateArtifact| { + let next = prior_state.extend(frozen_root); + // Phantom nullifier for the just-folded epoch (= prior evt). + let eta = commitment + .nullifier(&sk, prior_state.epoch_validated_through) + .0; + let w = frozen.non_membership_witness(eta).expect("non-membership"); + let mut i = ChainUpdateInput { + commitment: field_to_decimal(commitment.0), + epoch_created: "0".into(), + epoch_validated_through: field_to_decimal(next.epoch_validated_through.as_field()), + accumulator: field_to_decimal(next.accumulator), + fixed_vk_hash: vk_hash.clone(), + is_base_case: false, + prior_vk: vk_fields.clone(), + prior_proof: prior_art.proof.clone(), + prior_public_inputs: prior_art.public_inputs.clone(), + frozen_root_next: field_to_decimal(frozen_root), + spending_key: "0".into(), + token: "0".into(), + amount: "0".into(), + salt: "0".into(), + low_value: field_to_decimal(w.low_leaf.value), + low_next_value: field_to_decimal(w.low_leaf.next_value), + low_next_index: w.low_leaf.next_index.to_string(), + path_bits: path_bits_of(w.low_leaf_index), + siblings: siblings_of(&w), + }; + note_fields(&mut i); + (next, i) + }; + + // --- Step 1: verify genesis, fold epoch 0 (evt 0 -> 1) --- + let (state1, in1) = step(genesis, &genesis_art); + let art1 = prover.prove_chain_update(&in1).expect("prove step1 (verifies genesis)"); + + // --- Step 2: verify step1, fold epoch 1 (evt 1 -> 2) --- + let (state2, in2) = step(state1, &art1); + let _art2 = prover.prove_chain_update(&in2).expect("prove step2 (verifies step1)"); + + assert_eq!(state2.epoch_validated_through, Epoch(2)); + } + + /// The crux de-risk: an EVM-targeted `transfer` proof that *recursively + /// verifies* a real chain proof. Builds a genesis chain proof for the real + /// input note, then a 2-in-2-out transfer at epoch 0 (input_0 in a + /// single-leaf tree so `commitment_root = commitment_in_0` with no siblings; + /// input_1 a zero/padding note whose recursive verify the circuit skips), + /// proves it with `-t evm`, and verifies the proof. Success means the + /// spend-recursion-through-an-EVM-verifier path works end-to-end. + #[test] + #[ignore = "shells nargo+bb; proves the recursive transfer circuit end-to-end (minutes)"] + fn transfer_proof_verifies_with_recursive_chain_proof() { + let prover = BbProver::new(project_root()); + + // --- Inner: chain-update VK + a genesis chain proof for the real input. --- + let (vk_fields, vk_hash) = prover.write_chain_update_vk().expect("chain vk"); + + let sk = SpendingKey::from_bytes([7u8; 32]); + let owner = sk.derive_owner_pubkey(); + let token = Address::repeat_byte(0x11); + let salt0 = B256::repeat_byte(0x05); + let note0 = Note::with_salt(token, U256::from(100u64), owner, salt0, Epoch(0)); + let commitment0 = note0.commitment(); + + let genesis = ChainUpdateInput { + commitment: field_to_decimal(commitment0.0), + epoch_created: "0".into(), + epoch_validated_through: "0".into(), + accumulator: "0".into(), + fixed_vk_hash: vk_hash.clone(), + is_base_case: true, + prior_vk: zeros(ULTRA_VK_LENGTH_IN_FIELDS), + prior_proof: zeros(RECURSIVE_PROOF_LENGTH), + prior_public_inputs: zeros(CHAIN_PROOF_PUB_LEN), + frozen_root_next: "0".into(), + spending_key: field_to_decimal(sk.0), + token: token_field(token), + amount: field_to_decimal(B256::from(U256::from(100u64))), + salt: field_to_decimal(salt0), + low_value: "0".into(), + low_next_value: "0".into(), + low_next_index: "0".into(), + path_bits: vec![false; 32], + siblings: zeros(32), + }; + let chain0 = prover.prove_chain_update(&genesis).expect("genesis chain proof"); + + // --- Outer: a 2-in-2-out transfer at epoch 0, verifying chain0 recursively. --- + prover.write_transfer_vk().expect("transfer vk"); + + let salt1 = B256::repeat_byte(0x09); + let note1 = Note::with_salt(token, U256::ZERO, owner, salt1, Epoch(0)); + let commitment1 = note1.commitment(); + let out0 = Note::with_salt(token, U256::from(60u64), owner, B256::repeat_byte(0x01), Epoch(0)); + let out1 = Note::with_salt(token, U256::from(40u64), owner, B256::repeat_byte(0x02), Epoch(0)); + + let input = TransferInput { + nullifier_active_0: field_to_decimal(commitment0.nullifier(&sk, Epoch(0)).0), + nullifier_active_1: field_to_decimal(commitment1.nullifier(&sk, Epoch(0)).0), + commitment_out_0: field_to_decimal(out0.commitment().0), + commitment_out_1: field_to_decimal(out1.commitment().0), + commitment_root: field_to_decimal(commitment0.0), + current_epoch: "0".into(), + chain_vk_hash: vk_hash.clone(), + epoch_created_in_0: "0".into(), + epoch_created_in_1: "0".into(), + chain_accumulator_in_0: "0".into(), + chain_accumulator_in_1: "0".into(), + spending_key: field_to_decimal(sk.0), + token_in_0: token_field(token), + amount_in_0: 100, + salt_in_0: field_to_decimal(salt0), + token_in_1: token_field(token), + amount_in_1: 0, + salt_in_1: field_to_decimal(salt1), + token_out_0: token_field(token), + amount_out_0: 60, + owner_out_0: field_to_decimal(owner.0), + salt_out_0: field_to_decimal(B256::repeat_byte(0x01)), + token_out_1: token_field(token), + amount_out_1: 40, + owner_out_1: field_to_decimal(owner.0), + salt_out_1: field_to_decimal(B256::repeat_byte(0x02)), + proof_length: 0, + path_0: zeros(32), + indices_0: vec![false; 32], + path_1: zeros(32), + indices_1: vec![false; 32], + chain_vk: vk_fields.clone(), + chain_proof_0: chain0.proof.clone(), + chain_pub_0: chain0.public_inputs.clone(), + chain_proof_1: zeros(RECURSIVE_PROOF_LENGTH), + chain_pub_1: zeros(CHAIN_PROOF_PUB_LEN), + }; + + let proof = prover.prove_transfer(&input).expect("prove transfer (recursive)"); + assert_eq!(proof.public_inputs.len(), TRANSFER_PUB_LEN * 32, "11 public inputs"); + assert!(prover.verify_transfer_evm().expect("verify"), "transfer proof must verify"); + } + + /// The (non-recursive) deposit proof generates and verifies under `-t evm`. + #[test] + #[ignore = "shells nargo+bb; proves the deposit circuit (-t evm)"] + fn deposit_proof_verifies() { + let prover = BbProver::new(project_root()); + prover.write_evm_vk("deposit").expect("deposit vk"); + + let sk = SpendingKey::from_bytes([3u8; 32]); + let owner = sk.derive_owner_pubkey(); + let token = Address::repeat_byte(0x22); + let salt = B256::repeat_byte(0x07); + let note = Note::with_salt(token, U256::from(1000u64), owner, salt, Epoch(0)); + + let input = DepositInput { + commitment: field_to_decimal(note.commitment().0), + token: token_field(token), + amount: field_to_decimal(B256::from(U256::from(1000u64))), + current_epoch_at_deposit: 0, + owner_pubkey: field_to_decimal(owner.0), + salt: field_to_decimal(salt), + }; + + prover.prove_evm("deposit", &input).expect("prove deposit"); + assert!(prover.verify_evm("deposit").expect("verify"), "deposit proof must verify"); + } + + /// The (non-recursive) insertion proof generates and verifies under `-t evm`, + /// for a real 2-insertion built off the indexed-Merkle tree — exercising the + /// `InsertionWitness` -> `Prover.toml` mapping the relayer will use. + #[test] + #[ignore = "shells nargo+bb; proves the insertion circuit (-t evm)"] + fn insertion_proof_verifies() { + let prover = BbProver::new(project_root()); + prover.write_evm_vk("insertion").expect("insertion vk"); + + let mut tree = IndexedMerkleTree::new(); + let pre_root = tree.root(); + let pre_leaf_count = tree.leaf_count(); + let n0 = B256::from(U256::from(111u64)); + let n1 = B256::from(U256::from(222u64)); + let w0 = tree.insert(n0).expect("insert n0"); + let w1 = tree.insert(n1).expect("insert n1"); + let post_root = tree.root(); + + let input = + InsertionToml::from_witnesses(pre_root, post_root, pre_leaf_count, &[n0, n1], &[w0, w1]); + prover.prove_evm("insertion", &input).expect("prove insertion"); + assert!(prover.verify_evm("insertion").expect("verify"), "insertion proof must verify"); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/adapters/commitment_pir.rs b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/commitment_pir.rs new file mode 100644 index 0000000..6fccefe --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/commitment_pir.rs @@ -0,0 +1,136 @@ +//! tree-pir layout for the commitment tree. +//! +//! Flattens every LeanIMT node into a level-major array and packs each 32-byte +//! node into 16-bit limbs (records `< 2^17`, SimplePIR's plaintext bound), then +//! builds a [`PirDatabase`]. The server hosts this; the wallet — which knows its +//! own leaf index — computes the sibling-node record offsets with the SAME layout +//! plus `stateless_path`, so it fetches its membership path obliviously (the +//! offset math here is shared with the client adapter in the next slice). +//! +//! The layout is deterministic in the leaf count alone: `level_sizes` mirrors +//! LeanIMT's ceil-halving structure, so the client reproduces every offset +//! without seeing the tree. + +use alloy::primitives::B256; + +use crate::adapters::{ + merkle_tree::CommitmentTree, + pir::PirDatabase, +}; + +/// 16-bit limbs per 32-byte node (256 / 16). Each limb is `< 2^16 < 2^17`, so it +/// fits a SimplePIR record under the `2^17` plaintext modulus. +pub const LIMBS_PER_NODE: usize = 16; + +/// Node counts per level of a LeanIMT with `leaf_count` leaves: level 0 = +/// `leaf_count`, each higher level = `ceil(prev / 2)`, down to the single root. +/// Matches LeanIMT's node structure and `stateless_path`'s recurrence, so the +/// client reproduces it from the leaf count alone. +pub fn level_sizes(leaf_count: usize) -> Vec { + if leaf_count == 0 { + return Vec::new(); + } + let mut sizes = vec![leaf_count]; + let mut size = leaf_count; + while size > 1 { + size = size.div_ceil(2); + sizes.push(size); + } + sizes +} + +/// Flat (level-major) position of node `(level, index)`: all lower levels' nodes, +/// then `index`. Its 16 limbs occupy records `[offset*16, offset*16 + 16)`. +pub fn node_offset(level_sizes: &[usize], level: usize, index: usize) -> usize { + level_sizes[..level].iter().sum::() + index +} + +/// PIR record index of limb `limb` of node `(level, index)`. +pub fn record_index(level_sizes: &[usize], level: usize, index: usize, limb: usize) -> usize { + node_offset(level_sizes, level, index) * LIMBS_PER_NODE + limb +} + +/// Split a 32-byte node into 16 big-endian 16-bit limbs. +pub fn node_to_limbs(node: B256) -> [u64; LIMBS_PER_NODE] { + let mut limbs = [0u64; LIMBS_PER_NODE]; + for (i, limb) in limbs.iter_mut().enumerate() { + *limb = u16::from_be_bytes([node.0[2 * i], node.0[2 * i + 1]]) as u64; + } + limbs +} + +/// Reassemble 16 big-endian 16-bit limbs into a 32-byte node. +pub fn limbs_to_node(limbs: &[u64]) -> B256 { + let mut bytes = [0u8; 32]; + for (i, limb) in limbs.iter().take(LIMBS_PER_NODE).enumerate() { + let be = (*limb as u16).to_be_bytes(); + bytes[2 * i] = be[0]; + bytes[2 * i + 1] = be[1]; + } + B256::from(bytes) +} + +/// Flatten the commitment tree's nodes (level-major) and pack them into a +/// SimplePIR database of 16-bit-limb records. Records are laid out in exactly the +/// order [`node_offset`] / [`record_index`] address, so a client fetch at +/// `record_index(level_sizes(leaf_count), level, index, limb)` returns the right +/// limb. (Build is the expensive offline step — SimplePIR `setup`.) +pub fn build_commitment_pir_db(tree: &CommitmentTree) -> PirDatabase { + let mut records = Vec::new(); + for level in tree.nodes() { + for node in level { + records.extend_from_slice(&node_to_limbs(B256::from_slice(node))); + } + } + PirDatabase::from_records(records) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn limb_roundtrip() { + for node in [B256::ZERO, B256::repeat_byte(0xAB), B256::from_slice(&(0u8..32).collect::>())] { + assert_eq!(limbs_to_node(&node_to_limbs(node)), node); + } + } + + #[test] + fn level_sizes_follow_ceil_halving() { + assert_eq!(level_sizes(0), Vec::::new()); + assert_eq!(level_sizes(1), vec![1]); + assert_eq!(level_sizes(5), vec![5, 3, 2, 1]); + assert_eq!(level_sizes(8), vec![8, 4, 2, 1]); + } + + /// Every commitment-tree node round-trips through the flattened PIR database: + /// `record_index` + an oblivious `fetch` of its 16 limbs reassembles the node. + /// Also pins the layout invariant the client relies on: the computed + /// `level_sizes` equal the tree's actual per-level node counts. + #[test] + fn flattened_db_recovers_every_node() { + let mut tree = CommitmentTree::new(); + for i in 1..=5u8 { + tree.insert(&[i; 32]); + } + let sizes = level_sizes(tree.len()); + + let actual: Vec = tree.nodes().iter().map(|level| level.len()).collect(); + assert_eq!(actual, sizes, "computed level sizes must match the tree's node levels"); + + let db = build_commitment_pir_db(&tree); + for (level, level_nodes) in tree.nodes().iter().enumerate() { + for index in 0..level_nodes.len() { + let limbs: Vec = (0..LIMBS_PER_NODE) + .map(|limb| db.fetch(record_index(&sizes, level, index, limb))) + .collect(); + assert_eq!( + limbs_to_node(&limbs), + B256::from_slice(&tree.nodes()[level][index]), + "node ({level}, {index}) must round-trip through PIR", + ); + } + } + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/adapters/ethereum_rpc.rs b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/ethereum_rpc.rs new file mode 100644 index 0000000..0cb624e --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/ethereum_rpc.rs @@ -0,0 +1,344 @@ +//! Alloy-backed adapter for `ShieldedPoolExt` (and the mock ERC-20), used by the +//! in-process e2e to drive the contract on anvil. +//! +//! Differs from the parent's RPC adapter: no attestation registry and no on-chain +//! nullifier set (the extension keeps neither); adds the epoch / active-tree views +//! (`currentEpoch`, `activeNullifierRoot`, `activeLeafCount`, `frozenNullifierRoots`, +//! `expectedChainAccumulator`, `chainVkHash`) and the two-proof `transfer` / +//! `withdraw` signatures (wallet spend proof + relayer insertion proof). Methods +//! are inherent: the single-backend PoC does not need an on-chain port trait. + +use alloy::{ + network::EthereumWallet, + primitives::{ + Address, + B256, + Bytes, + U256, + }, + providers::{ + DynProvider, + ProviderBuilder, + }, + signers::local::PrivateKeySigner, + sol, +}; + +// Contract bindings (function signatures mirror `contracts/src/ShieldedPoolExt.sol`). +sol! { + #[sol(rpc)] + interface IShieldedPoolExt { + function commitmentRoot() external view returns (bytes32); + function getCommitmentCount() external view returns (uint256); + function isKnownRoot(bytes32 root) external view returns (bool); + function supportedTokens(address token) external view returns (bool); + function currentEpoch() external view returns (uint64); + function activeNullifierRoot() external view returns (bytes32); + function activeLeafCount() external view returns (uint64); + function frozenNullifierRoots(uint64 epoch) external view returns (bytes32); + function expectedChainAccumulator(uint64 epochCreated) external view returns (bytes32); + function chainVkHash() external view returns (bytes32); + + function deposit( + bytes proof, + bytes32 commitment, + address token, + uint256 amount, + bytes encryptedNote + ) external; + + function transfer( + bytes spendProof, + bytes insertionProof, + bytes32[2] nullifiers, + bytes32[2] outputCommitments, + bytes32 root, + uint64[2] epochCreated, + bytes32 postActiveRoot, + bytes encryptedNotes + ) external; + + function withdraw( + bytes spendProof, + bytes insertionProof, + bytes32 nullifier, + address token, + uint256 amount, + address recipient, + bytes32 root, + uint64 epochCreated, + bytes32 postActiveRoot + ) external; + + function rolloverEpoch() external; + function addSupportedToken(address token) external; + } + + #[sol(rpc)] + interface IMockERC20 { + function mint(address to, uint256 amount) external; + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + } +} + +/// Errors from on-chain interaction. +#[derive(Debug, thiserror::Error)] +pub enum OnChainError { + #[error("signer: {0}")] + Signer(String), + #[error("rpc: {0}")] + Rpc(String), + #[error("contract call: {0}")] + Contract(String), + #[error("tx failed: {0}")] + TransactionFailed(String), + #[error("tx reverted: {0}")] + TransactionReverted(String), +} + +/// Minimal transaction receipt. +#[derive(Debug, Clone)] +pub struct TxReceipt { + pub tx_hash: B256, + pub block_number: u64, + pub gas_used: u64, + pub success: bool, +} + +impl From for TxReceipt { + fn from(receipt: alloy::rpc::types::TransactionReceipt) -> Self { + Self { + tx_hash: receipt.transaction_hash, + block_number: receipt.block_number.unwrap_or(0), + gas_used: receipt.gas_used, + success: receipt.status(), + } + } +} + +/// RPC adapter for `ShieldedPoolExt`. +pub struct EthereumRpc { + provider: DynProvider, + pool: Address, + signer_address: Address, +} + +impl EthereumRpc { + /// Connect with a signing key and the deployed `ShieldedPoolExt` address. + pub async fn new(rpc_url: &str, private_key: &str, pool: Address) -> Result { + let signer: PrivateKeySigner = private_key + .parse() + .map_err(|e| OnChainError::Signer(format!("invalid private key: {e}")))?; + let signer_address = signer.address(); + let wallet = EthereumWallet::from(signer); + let provider = DynProvider::new( + ProviderBuilder::new().wallet(wallet).connect_http( + rpc_url.parse().map_err(|e| OnChainError::Rpc(format!("invalid RPC URL: {e}")))?, + ), + ); + Ok(Self { provider, pool, signer_address }) + } + + pub fn signer_address(&self) -> Address { + self.signer_address + } + + pub fn pool_address(&self) -> Address { + self.pool + } + + fn pool(&self) -> IShieldedPoolExt::IShieldedPoolExtInstance<&DynProvider> { + IShieldedPoolExt::new(self.pool, &self.provider) + } + + // ===== Reads ===== + + pub async fn commitment_root(&self) -> Result { + self.pool().commitmentRoot().call().await.map_err(|e| OnChainError::Contract(e.to_string())) + } + + pub async fn commitment_count(&self) -> Result { + let n = self.pool().getCommitmentCount().call().await.map_err(|e| OnChainError::Contract(e.to_string()))?; + Ok(n.try_into().unwrap_or(u64::MAX)) + } + + pub async fn is_known_root(&self, root: B256) -> Result { + self.pool().isKnownRoot(root).call().await.map_err(|e| OnChainError::Contract(e.to_string())) + } + + pub async fn is_token_supported(&self, token: Address) -> Result { + self.pool().supportedTokens(token).call().await.map_err(|e| OnChainError::Contract(e.to_string())) + } + + pub async fn current_epoch(&self) -> Result { + self.pool().currentEpoch().call().await.map_err(|e| OnChainError::Contract(e.to_string())) + } + + pub async fn active_nullifier_root(&self) -> Result { + self.pool().activeNullifierRoot().call().await.map_err(|e| OnChainError::Contract(e.to_string())) + } + + pub async fn active_leaf_count(&self) -> Result { + self.pool().activeLeafCount().call().await.map_err(|e| OnChainError::Contract(e.to_string())) + } + + pub async fn frozen_nullifier_root(&self, epoch: u64) -> Result { + self.pool().frozenNullifierRoots(epoch).call().await.map_err(|e| OnChainError::Contract(e.to_string())) + } + + pub async fn expected_chain_accumulator(&self, epoch_created: u64) -> Result { + self.pool().expectedChainAccumulator(epoch_created).call().await.map_err(|e| OnChainError::Contract(e.to_string())) + } + + pub async fn chain_vk_hash(&self) -> Result { + self.pool().chainVkHash().call().await.map_err(|e| OnChainError::Contract(e.to_string())) + } + + // ===== Writes ===== + + pub async fn deposit( + &self, + proof: Bytes, + commitment: B256, + token: Address, + amount: U256, + encrypted_note: Bytes, + ) -> Result { + let pending = self + .pool() + .deposit(proof, commitment, token, amount, encrypted_note) + .send() + .await + .map_err(|e| OnChainError::TransactionFailed(e.to_string()))?; + self.confirm(pending, "deposit").await + } + + /// Two-proof transfer: wallet spend proof + relayer insertion proof. The + /// `nullifiers` list is fed to both verifiers by the contract (cross-proof + /// binding); `post_active_root` is the insertion proof's attested new root. + #[allow(clippy::too_many_arguments)] + pub async fn transfer( + &self, + spend_proof: Bytes, + insertion_proof: Bytes, + nullifiers: [B256; 2], + output_commitments: [B256; 2], + root: B256, + epoch_created: [u64; 2], + post_active_root: B256, + encrypted_notes: Bytes, + ) -> Result { + let pending = self + .pool() + .transfer( + spend_proof, + insertion_proof, + nullifiers, + output_commitments, + root, + epoch_created, + post_active_root, + encrypted_notes, + ) + .send() + .await + .map_err(|e| OnChainError::TransactionFailed(e.to_string()))?; + self.confirm(pending, "transfer").await + } + + #[allow(clippy::too_many_arguments)] + pub async fn withdraw( + &self, + spend_proof: Bytes, + insertion_proof: Bytes, + nullifier: B256, + token: Address, + amount: U256, + recipient: Address, + root: B256, + epoch_created: u64, + post_active_root: B256, + ) -> Result { + let pending = self + .pool() + .withdraw( + spend_proof, + insertion_proof, + nullifier, + token, + amount, + recipient, + root, + epoch_created, + post_active_root, + ) + .send() + .await + .map_err(|e| OnChainError::TransactionFailed(e.to_string()))?; + self.confirm(pending, "withdraw").await + } + + pub async fn rollover_epoch(&self) -> Result { + let pending = self + .pool() + .rolloverEpoch() + .send() + .await + .map_err(|e| OnChainError::TransactionFailed(e.to_string()))?; + self.confirm(pending, "rolloverEpoch").await + } + + pub async fn add_supported_token(&self, token: Address) -> Result { + let pending = self + .pool() + .addSupportedToken(token) + .send() + .await + .map_err(|e| OnChainError::TransactionFailed(e.to_string()))?; + self.confirm(pending, "addSupportedToken").await + } + + // ===== ERC-20 (mock token) ===== + + pub async fn approve_token(&self, token: Address, amount: U256) -> Result { + let erc20 = IMockERC20::new(token, &self.provider); + let pending = erc20 + .approve(self.pool, amount) + .send() + .await + .map_err(|e| OnChainError::TransactionFailed(e.to_string()))?; + self.confirm(pending, "approve").await + } + + pub async fn mint_mock_token(&self, token: Address, to: Address, amount: U256) -> Result { + let erc20 = IMockERC20::new(token, &self.provider); + let pending = erc20 + .mint(to, amount) + .send() + .await + .map_err(|e| OnChainError::TransactionFailed(e.to_string()))?; + self.confirm(pending, "mint").await + } + + pub async fn get_token_balance(&self, token: Address, account: Address) -> Result { + let erc20 = IMockERC20::new(token, &self.provider); + erc20.balanceOf(account).call().await.map_err(|e| OnChainError::Contract(e.to_string())) + } + + /// Await a sent tx's receipt and require success. + async fn confirm( + &self, + pending: alloy::providers::PendingTransactionBuilder, + what: &str, + ) -> Result { + let receipt = pending + .get_receipt() + .await + .map_err(|e| OnChainError::TransactionFailed(e.to_string()))?; + if !receipt.status() { + return Err(OnChainError::TransactionReverted(format!("{what} reverted"))); + } + Ok(receipt.into()) + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/adapters/indexed_merkle_tree.rs b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/indexed_merkle_tree.rs new file mode 100644 index 0000000..642e522 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/indexed_merkle_tree.rs @@ -0,0 +1,293 @@ +//! Off-chain indexed Merkle tree mirror. +//! +//! Maintains the leaves of an active or frozen nullifier tree, computes its +//! root, and generates the witnesses the circuits consume: +//! [`NonMembershipWitness`] (chain-update, over frozen trees) and +//! [`InsertionWitness`] (insertion circuit, over the active tree). The hashing +//! and witness shapes live in [`crate::domain::indexed_merkle`]; this is the +//! stateful container that produces them. +//! +//! Convention: index 0 is the genesis leaf `(0, 0, 0)`, which doubles as the +//! empty-slot value and the bootstrap low-leaf covering `[0, +inf)`. Real +//! nullifiers append at indices 1, 2, ..., so [`leaf_count`] starts at 1 and an +//! empty tree's root equals [`empty_root`]. +//! +//! PoC note: `root()` and `merkle_path()` recompute the occupied levels on each +//! call (O(occupied) hashing); fine at PoC scale. Production would persist the +//! node array and update only the O(depth) nodes on each insert's path +//! (incremental Merkle update), making `root()`/`merkle_path()` O(1)/O(depth). +//! +//! [`leaf_count`]: IndexedMerkleTree::leaf_count +//! [`empty_root`]: IndexedMerkleTree::empty_root +//! [`NonMembershipWitness`]: crate::domain::indexed_merkle::NonMembershipWitness +//! [`InsertionWitness`]: crate::domain::indexed_merkle::InsertionWitness + +use alloy::primitives::B256; + +use crate::{ + crypto::poseidon::poseidon2, + domain::indexed_merkle::{ + IndexedLeaf, + InsertionWitness, + NonMembershipWitness, + NULLIFIER_TREE_DEPTH, + covers, + empty_leaf_hash, + }, +}; + +/// Errors from inserting into the off-chain tree. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum IndexedTreeError { + #[error("value 0 is reserved for the empty/genesis leaf")] + ReservedZeroValue, + #[error("value already present in the tree")] + DuplicateValue, +} + +/// An off-chain indexed Merkle tree of fixed depth [`NULLIFIER_TREE_DEPTH`]. +#[derive(Clone)] +pub struct IndexedMerkleTree { + /// Occupied leaves in index order (index 0 = genesis leaf). + leaves: Vec, + /// Zero-subtree hash per level: `zero_hashes[0]` = empty-leaf hash, and + /// `zero_hashes[l+1] = poseidon2(zero_hashes[l], zero_hashes[l])`. + zero_hashes: Vec, +} + +impl Default for IndexedMerkleTree { + fn default() -> Self { + Self::new() + } +} + +impl IndexedMerkleTree { + /// A fresh tree holding only the genesis leaf at index 0. + pub fn new() -> Self { + let mut zero_hashes = Vec::with_capacity(NULLIFIER_TREE_DEPTH + 1); + zero_hashes.push(empty_leaf_hash()); + for level in 0..NULLIFIER_TREE_DEPTH { + let prev = zero_hashes[level]; + zero_hashes.push(poseidon2(prev, prev)); + } + Self { + leaves: vec![IndexedLeaf::EMPTY], + zero_hashes, + } + } + + /// Number of occupied leaves (genesis + appended); also the next free index. + pub fn leaf_count(&self) -> u64 { + self.leaves.len() as u64 + } + + /// Root of a fully-empty tree (the on-chain `EMPTY_IMT_ROOT` constant). + pub fn empty_root(&self) -> B256 { + self.zero_hashes[NULLIFIER_TREE_DEPTH] + } + + /// Current root. + pub fn root(&self) -> B256 { + self.levels().pop().expect("levels has DEPTH+1 entries")[0] + } + + /// True if `value` is already present as a leaf. + pub fn contains(&self, value: B256) -> bool { + self.leaves.iter().any(|leaf| leaf.value == value) + } + + /// Index of the low-leaf whose range strictly covers `value`, or `None` if + /// `value` is already present (no leaf covers an existing value). + pub fn low_leaf_index(&self, value: B256) -> Option { + self.leaves + .iter() + .position(|leaf| covers(leaf, value)) + .map(|i| i as u64) + } + + /// Sorted-low-leaf non-membership witness for `value` against the current + /// root, or `None` if `value` is present. + pub fn non_membership_witness(&self, value: B256) -> Option { + let low_leaf_index = self.low_leaf_index(value)?; + Some(NonMembershipWitness { + low_leaf: self.leaves[low_leaf_index as usize], + low_leaf_index, + siblings: self.merkle_path(low_leaf_index), + }) + } + + /// Insert `value`, mutating the predecessor and appending the new leaf. + /// Returns the [`InsertionWitness`] advancing the pre-root to the new root. + pub fn insert(&mut self, value: B256) -> Result { + if value == B256::ZERO { + return Err(IndexedTreeError::ReservedZeroValue); + } + let low_leaf_index = self + .low_leaf_index(value) + .ok_or(IndexedTreeError::DuplicateValue)?; + + let low_leaf = self.leaves[low_leaf_index as usize]; + let new_leaf_index = self.leaves.len() as u64; + + // Predecessor path against the pre-state root. + let low_leaf_siblings = self.merkle_path(low_leaf_index); + + // Mutate the predecessor to point at the new leaf (tree now at r'). + self.leaves[low_leaf_index as usize].next_value = value; + self.leaves[low_leaf_index as usize].next_index = new_leaf_index; + + // Empty-slot path for the append index against r'. + let new_leaf_siblings = self.merkle_path(new_leaf_index); + + // Write the new leaf, inheriting the predecessor's old forward pointer. + self.leaves.push(IndexedLeaf { + value, + next_value: low_leaf.next_value, + next_index: low_leaf.next_index, + }); + + Ok(InsertionWitness { + low_leaf, + low_leaf_index, + low_leaf_siblings, + new_leaf_index, + new_leaf_siblings, + }) + } + + /// Compute the occupied-prefix node hashes per level. `levels[0]` is the leaf + /// hashes; `levels[NULLIFIER_TREE_DEPTH][0]` is the root. + fn levels(&self) -> Vec> { + let mut levels: Vec> = Vec::with_capacity(NULLIFIER_TREE_DEPTH + 1); + levels.push(self.leaves.iter().map(IndexedLeaf::hash).collect()); + for level in 0..NULLIFIER_TREE_DEPTH { + let current = &levels[level]; + let mut next = Vec::with_capacity(current.len().div_ceil(2)); + let mut i = 0; + while i < current.len() { + let left = current[i]; + let right = if i + 1 < current.len() { + current[i + 1] + } else { + self.zero_hashes[level] + }; + next.push(poseidon2(left, right)); + i += 2; + } + levels.push(next); + } + levels + } + + /// Sibling path for `index` (length [`NULLIFIER_TREE_DEPTH`]). Siblings in the + /// empty region of the tree come from `zero_hashes`. + fn merkle_path(&self, index: u64) -> Vec { + let levels = self.levels(); + let mut path = Vec::with_capacity(NULLIFIER_TREE_DEPTH); + let mut idx = index as usize; + for (level, nodes) in levels.iter().take(NULLIFIER_TREE_DEPTH).enumerate() { + let sibling_idx = idx ^ 1; + let sibling = nodes + .get(sibling_idx) + .copied() + .unwrap_or(self.zero_hashes[level]); + path.push(sibling); + idx >>= 1; + } + path + } +} + +#[cfg(test)] +mod tests { + use alloy::primitives::U256; + + use super::*; + + fn v(n: u64) -> B256 { + B256::from(U256::from(n)) + } + + #[test] + fn empty_tree_root_matches_empty_root() { + let tree = IndexedMerkleTree::new(); + assert_eq!(tree.leaf_count(), 1, "genesis leaf occupies index 0"); + assert_eq!(tree.root(), tree.empty_root()); + } + + #[test] + fn insertion_witness_reconstructs_new_root() { + let mut tree = IndexedMerkleTree::new(); + // Insert out of order to exercise predecessor lookup. + for value in [v(50), v(10), v(90), v(30)] { + let pre_root = tree.root(); + let witness = tree.insert(value).unwrap(); + let post_root = tree.root(); + assert_eq!( + witness.verify_and_apply(pre_root, value), + Some(post_root), + "insertion witness must advance pre_root to the tree's new root" + ); + } + } + + #[test] + fn non_membership_witness_verifies_for_absent_values() { + let mut tree = IndexedMerkleTree::new(); + for value in [v(10), v(20), v(30)] { + tree.insert(value).unwrap(); + } + let root = tree.root(); + + // Absent value between existing leaves. + let absent = v(25); + let witness = tree.non_membership_witness(absent).unwrap(); + assert!(witness.verify(root, absent)); + + // A present value has no non-membership witness. + assert!(tree.non_membership_witness(v(20)).is_none()); + assert!(tree.contains(v(20))); + } + + #[test] + fn linked_list_stays_sorted() { + let mut tree = IndexedMerkleTree::new(); + let inserted = [v(50), v(10), v(90), v(30), v(70)]; + for value in inserted { + tree.insert(value).unwrap(); + } + + // Walk the linked list from the genesis leaf following next_index. + let mut walk = Vec::new(); + let mut leaf = tree.leaves[0]; + while leaf.next_value != B256::ZERO { + walk.push(leaf.next_value); + leaf = tree.leaves[leaf.next_index as usize]; + } + let mut expected: Vec = inserted.to_vec(); + expected.sort_by_key(|b| U256::from_be_bytes(b.0)); + assert_eq!(walk, expected, "values must be linked in ascending order"); + } + + #[test] + fn deterministic_root_across_identical_sequences() { + let mut a = IndexedMerkleTree::new(); + let mut b = IndexedMerkleTree::new(); + for value in [v(7), v(3), v(11)] { + a.insert(value).unwrap(); + b.insert(value).unwrap(); + } + assert_eq!(a.root(), b.root()); + } + + #[test] + fn rejects_duplicate_and_zero() { + let mut tree = IndexedMerkleTree::new(); + tree.insert(v(42)).unwrap(); + assert_eq!(tree.insert(v(42)), Err(IndexedTreeError::DuplicateValue)); + assert_eq!( + tree.insert(B256::ZERO), + Err(IndexedTreeError::ReservedZeroValue) + ); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/adapters/light_client.rs b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/light_client.rs new file mode 100644 index 0000000..f3277f9 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/light_client.rs @@ -0,0 +1,89 @@ +//! Two-level MPT verification of a contract storage slot (the SPEC's `verify_mpt`) +//! plus a [`RootVerifier`] over a trusted state root. +//! +//! `verify_account_storage` checks an `eth_getProof` response against a state +//! root: level 1 proves the account leaf under the state root (binding the +//! account's `storage_hash`); level 2 proves the requested slot under that +//! `storage_hash`. In production the state root comes from a Helios finalized +//! header (consensus-verified); [`TrustedRootVerifier`] instead trusts a supplied +//! root (e.g. an anvil block's `stateRoot`), since Helios needs a beacon chain +//! anvil lacks. The verification is identical regardless of the root's provenance. + +use alloy::{ + primitives::{ + keccak256, + B256, + }, + rpc::types::EIP1186AccountProofResponse, +}; +use alloy_trie::{ + proof::verify_proof, + Nibbles, + TrieAccount, +}; + +use crate::ports::root_verifier::{ + RootVerifier, + RootVerifierError, +}; + +/// Verify `proof` (an `eth_getProof` result for one slot) against `state_root` +/// and return the proven storage value. +/// +/// Level 1 — the account leaf `rlp(nonce, balance, storage_hash, code_hash)` is +/// proven at `keccak256(address)` under `state_root`. Level 2 — the slot value is +/// proven at `keccak256(slot)` under the account's `storage_hash`. A zero value is +/// proven by *exclusion* (the slot is absent from the storage trie). +pub fn verify_account_storage( + state_root: B256, + proof: &EIP1186AccountProofResponse, +) -> Result { + let account = TrieAccount { + nonce: proof.nonce, + balance: proof.balance, + storage_root: proof.storage_hash, + code_hash: proof.code_hash, + }; + verify_proof( + state_root, + Nibbles::unpack(keccak256(proof.address.as_slice())), + Some(alloy_rlp::encode(&account)), + &proof.account_proof, + ) + .map_err(|e| RootVerifierError::Account(e.to_string()))?; + + let entry = proof.storage_proof.first().ok_or(RootVerifierError::MissingSlot)?; + // Ethereum RLP-encodes slot values with leading zeros trimmed; a zero slot is + // absent from the trie, so it's proven by exclusion (`expected = None`). + let expected = (!entry.value.is_zero()).then(|| alloy_rlp::encode(&entry.value)); + verify_proof( + proof.storage_hash, + Nibbles::unpack(keccak256(entry.key.as_b256().as_slice())), + expected, + &entry.proof, + ) + .map_err(|e| RootVerifierError::Storage(e.to_string()))?; + + Ok(B256::from(entry.value)) +} + +/// [`RootVerifier`] over a state root the caller already trusts. PoC/dev use: +/// pass an anvil block's `stateRoot`. Production: a `HeliosRootVerifier` would +/// hold the light client's consensus-verified finalized `state_root` and call the +/// same [`verify_account_storage`] — not built here, since Helios requires a +/// beacon chain that anvil does not provide. +pub struct TrustedRootVerifier { + state_root: B256, +} + +impl TrustedRootVerifier { + pub fn new(state_root: B256) -> Self { + Self { state_root } + } +} + +impl RootVerifier for TrustedRootVerifier { + fn verify_storage(&self, proof: &EIP1186AccountProofResponse) -> Result { + verify_account_storage(self.state_root, proof) + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/adapters/merkle_tree.rs b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/merkle_tree.rs new file mode 100644 index 0000000..590705d --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/merkle_tree.rs @@ -0,0 +1,133 @@ +//! LeanIMT commitment-tree mirror. +//! +//! Wraps the `lean-imt` crate to provide the Poseidon-based append-only +//! commitment tree, matching the on-chain LeanIMT and the Noir circuit's +//! `binary_merkle_root`. Ported from the parent shielded-pool (PoC independence); +//! the only change is that the node hasher delegates to +//! [`crate::crypto::poseidon::poseidon2`] so every Merkle node uses the same +//! Poseidon instance as the rest of the crate (and the circuits). + +use alloy::primitives::B256; +use lean_imt::hashed_tree::{ + HashedLeanIMT, + LeanIMTHasher, +}; + +use crate::{ + crypto::poseidon::poseidon2, + domain::merkle::CommitmentMerkleProof, +}; + +/// Poseidon (arity-2) hasher for the LeanIMT, delegating to the crate's +/// `poseidon2` so node hashes match the commitment/circuit Poseidon exactly. +#[derive(Debug, Default, Clone)] +pub struct PoseidonHash; + +impl LeanIMTHasher<32> for PoseidonHash { + fn hash(input: &[u8]) -> [u8; 32] { + // LeanIMT calls this with two concatenated 32-byte children. + let left = B256::from_slice(&input[..32]); + let right = B256::from_slice(&input[32..]); + poseidon2(left, right).0 + } +} + +/// Commitment tree using LeanIMT with Poseidon hashing. +pub struct CommitmentTree(HashedLeanIMT<32, PoseidonHash>); + +fn decode_path(index: usize, path_len: usize) -> Vec { + let mut path = Vec::with_capacity(path_len); + for i in 0..path_len { + path.push(((index >> i) & 1) as u8); // LSB first + } + path +} + +impl CommitmentTree { + /// Create a new empty commitment tree. + pub fn new() -> Self { + Self(HashedLeanIMT::new(&[], PoseidonHash).expect("empty LeanIMT init")) + } + + /// Insert a leaf (commitment hash) into the tree. + pub fn insert(&mut self, leaf: &[u8; 32]) { + self.0.insert(leaf); + } + + /// Number of leaves in the tree. + pub fn len(&self) -> usize { + self.0.size() + } + + /// Whether the tree is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Current root, or `None` if empty. + pub fn root(&self) -> Option<[u8; 32]> { + self.0.root() + } + + /// All tree nodes, level-major (`nodes()[level][index]`, level 0 = leaves). + /// The flattened source for the tree-pir database (see `commitment_pir`). + pub fn nodes(&self) -> &[Vec<[u8; 32]>] { + self.0.tree().nodes() + } + + /// Generate a membership proof for the leaf at `leaf_index`. + pub fn generate_commitment_proof(&self, leaf_index: u64) -> Option { + let proof = self.0.generate_proof(leaf_index as usize).ok()?; + let path: Vec = proof.siblings.iter().map(|s| B256::from_slice(s)).collect(); + let indices = decode_path(proof.index, proof.siblings.len()); + Some(CommitmentMerkleProof::new(path, indices, leaf_index)) + } +} + +impl Default for CommitmentTree { + fn default() -> Self { + Self::new() + } +} + +/// Raw bytes of a `B256` for tree insertion. +pub fn b256_to_bytes(value: &B256) -> [u8; 32] { + value.0 +} + +/// A `B256` from tree bytes. +pub fn bytes_to_b256(bytes: &[u8; 32]) -> B256 { + B256::from_slice(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_commitment_tree_basic() { + let mut tree = CommitmentTree::new(); + assert!(tree.is_empty()); + tree.insert(&[1u8; 32]); + assert_eq!(tree.len(), 1); + assert!(tree.root().is_some()); + } + + #[test] + fn test_commitment_tree_proof_generation() { + let mut tree = CommitmentTree::new(); + tree.insert(&[1u8; 32]); + tree.insert(&[2u8; 32]); + + let proof = tree.generate_commitment_proof(0).unwrap(); + assert_eq!(proof.leaf_index, 0); + assert_eq!(proof.proof_length, proof.path.len()); + assert!(!proof.path.is_empty()); + } + + #[test] + fn test_b256_conversion() { + let original = B256::repeat_byte(42); + assert_eq!(bytes_to_b256(&b256_to_bytes(&original)), original); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/adapters/pir.rs b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/pir.rs new file mode 100644 index 0000000..a5e9180 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/pir.rs @@ -0,0 +1,110 @@ +//! SimplePIR primitive: an oblivious fetch of a single `u64` record (each record +//! `< 2^MOD_POWER`). The commitment-path read is built on this — the +//! server's only input to `answer` is the encrypted query, so it never learns the +//! index. Server (`answer`) and client (`query`/`recover`) run in one process for +//! the PoC; that does not weaken the index-privacy property (the server's input is +//! identical either way). +//! +//! Shortcut vs SPEC: SimplePIR is NOT silent-preprocessing (the SPEC's InsPIRe). +//! The client keeps `client_hint` from the offline `setup`, and the offline phase +//! is per-database. InsPIRe (server-side silent preprocessing, no client hint) has +//! no portable Rust library (Google's impl is an Ubuntu/AVX-512 benchmark CLI), so +//! it stays the documented production target. See README "Implementation shortcuts". + +use simplepir::{ + answer, + query, + recover, + setup, + CompressedDatabase, + Database, + Matrix, +}; + +/// LWE secret-key dimension (SimplePIR's recommended value for security). +pub const SECRET_DIMENSION: usize = 2048; +/// Plaintext-modulus exponent: each record MUST be `< 2^MOD_POWER`. +pub const MOD_POWER: u8 = 17; + +/// Plaintext modulus `2^MOD_POWER` (one past the largest representable record). +pub fn plaintext_mod() -> u64 { + 1u64 << MOD_POWER +} + +/// An in-process SimplePIR database over `u64` records. Bundles the server side +/// (compressed data + `server_hint` seed) and the client side (`client_hint`); a +/// real deployment splits these across the wire, but index-privacy is identical — +/// the server only ever sees the encrypted `query_cipher`. +pub struct PirDatabase { + db: Database, + compressed: CompressedDatabase, + /// Seed for the public A-matrix (server-side, public). + server_hint: u64, + client_hint: Matrix, + side_len: usize, +} + +impl PirDatabase { + /// Build from row-major `records` (each MUST be `< 2^MOD_POWER`), padded to a + /// square matrix, running the (expensive, offline) `setup`. + pub fn from_records(records: Vec) -> Self { + let db = Database::from_vector(records, MOD_POWER); + let side_len = db.side_len(); + let (server_hint, client_hint) = setup(&db, SECRET_DIMENSION); + let compressed = db.compress().expect("compress database"); + Self { db, compressed, server_hint, client_hint, side_len } + } + + /// The square-matrix side length (capacity is `side_len^2`). + pub fn side_len(&self) -> usize { + self.side_len + } + + /// The plaintext record stored at `index` (server-side view; used to verify + /// an oblivious `fetch` returned the right value). + pub fn get(&self, index: usize) -> Option { + self.db.get(index) + } + + /// Obliviously fetch the record at `index` via the full PIR roundtrip: client + /// `query` -> server `answer` -> client `recover`. The server's only input is + /// the encrypted query, so it learns nothing about `index`. + pub fn fetch(&self, index: usize) -> u64 { + let (client_state, query_cipher) = + query(index, self.side_len, SECRET_DIMENSION, self.server_hint, plaintext_mod()); + let answer_cipher = answer(&self.compressed, &query_cipher); + recover(&client_state, &self.client_hint, &answer_cipher, &query_cipher, plaintext_mod()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The oblivious `fetch` recovers the stored record at every index — i.e. the + /// SimplePIR query -> answer -> recover roundtrip is correct for our params. + #[test] + fn fetch_recovers_each_stored_record() { + let records: Vec = (0..16).map(|i| 1000 + i as u64).collect(); + let pir = PirDatabase::from_records(records); + for index in 0..16 { + let expected = pir.get(index).expect("record present"); + assert_eq!(pir.fetch(index), expected, "PIR fetch must recover the stored record at {index}"); + } + } + + /// The server's only input is `query_cipher`; it must not reveal the index. + /// (a) The cipher size is identical for any index (no length side-channel). + /// (b) Two queries for the SAME index differ (probabilistic encryption — the + /// server cannot even tell two reads hit the same leaf). The residual + /// guarantee — that the ciphertexts are computationally indistinguishable — + /// rests on LWE semantic security and is not asserted here. + #[test] + fn query_does_not_leak_the_index() { + let pir = PirDatabase::from_records((0..16).map(|i| i as u64).collect()); + let mk = |index| query(index, pir.side_len, SECRET_DIMENSION, pir.server_hint, plaintext_mod()).1; + + assert_eq!(mk(0).len(), mk(7).len(), "query size must not depend on the index"); + assert_ne!(mk(3), mk(3), "repeated queries for one index must differ (semantic security)"); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/adapters/pir_client.rs b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/pir_client.rs new file mode 100644 index 0000000..044056a --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/pir_client.rs @@ -0,0 +1,160 @@ +//! Cleartext PIR client: a dev stand-in for the PIR'd commitment-path read. +//! +//! Fetches the membership path from the shared in-process state replica in clear +//! — the server learns the leaf index. The real [`SimplePirClient`] (below) +//! implements the same [`PirClient`] trait without revealing the index; callers +//! do not change. + +use std::sync::{ + Arc, + RwLock, +}; + +use lean_imt::stateless::stateless_path; + +use crate::{ + adapters::{ + commitment_pir::{ + build_commitment_pir_db, + level_sizes, + limbs_to_node, + record_index, + LIMBS_PER_NODE, + }, + merkle_tree::CommitmentTree, + pir::PirDatabase, + state_replica::StateReplica, + }, + domain::merkle::CommitmentMerkleProof, + ports::pir::{ + PirClient, + PirError, + }, +}; + +/// Cleartext PIR client over a shared in-process [`StateReplica`]. +pub struct CleartextPirClient { + replica: Arc>, +} + +impl CleartextPirClient { + pub fn new(replica: Arc>) -> Self { + Self { replica } + } +} + +impl PirClient for CleartextPirClient { + fn fetch_membership_path(&self, leaf_index: u64) -> Result { + let replica = self.replica.read().expect("replica lock poisoned"); + if replica.commitment_count() == 0 { + return Err(PirError::EmptyTree); + } + replica + .commitment_proof(leaf_index) + .ok_or(PirError::LeafOutOfRange(leaf_index)) + } +} + +/// Real SimplePIR commitment-path client. Holds a snapshot PIR database +/// of the commitment tree (the server side) plus the public leaf count. A fetch +/// derives the sibling positions from the leaf index via `stateless_path` — no +/// tree access — then PIR-fetches each sibling's limbs and reassembles the proof, +/// so the server only ever sees encrypted queries and never the leaf index. +/// +/// Drop-in for [`CleartextPirClient`]: same [`PirClient`] trait, same +/// `CommitmentMerkleProof` output (verified against the cleartext path in tests). +pub struct SimplePirClient { + /// `None` for an empty tree (nothing to read). + db: Option, + leaf_count: usize, +} + +impl SimplePirClient { + /// Snapshot the commitment tree into a SimplePIR database (runs the expensive + /// offline `setup`). + pub fn from_tree(tree: &CommitmentTree) -> Self { + let leaf_count = tree.len(); + let db = (leaf_count > 0).then(|| build_commitment_pir_db(tree)); + Self { db, leaf_count } + } +} + +impl PirClient for SimplePirClient { + fn fetch_membership_path(&self, leaf_index: u64) -> Result { + let db = self.db.as_ref().ok_or(PirError::EmptyTree)?; + // Sibling positions from the leaf index alone (no tree access). + let elements = stateless_path(leaf_index as usize, self.leaf_count) + .map_err(|_| PirError::LeafOutOfRange(leaf_index))?; + let sizes = level_sizes(self.leaf_count); + + let mut path = Vec::with_capacity(elements.len()); + let mut indices = Vec::with_capacity(elements.len()); + for element in &elements { + let mut limbs = [0u64; LIMBS_PER_NODE]; + for (limb, slot) in limbs.iter_mut().enumerate() { + *slot = db.fetch(record_index(&sizes, element.level(), element.sibling_index(), limb)); + } + path.push(limbs_to_node(&limbs)); + // `is_right` == the proven node was a right child => sibling on the + // left => index bit 1 (matches `CommitmentMerkleProof::reconstruct_root`). + indices.push(u8::from(element.is_right())); + } + Ok(CommitmentMerkleProof::new(path, indices, leaf_index)) + } +} + +#[cfg(test)] +mod tests { + use alloy::primitives::B256; + + use super::*; + + fn tree_with(n: u8) -> (CommitmentTree, Vec<[u8; 32]>) { + let mut tree = CommitmentTree::new(); + let leaves: Vec<[u8; 32]> = (1..=n).map(|i| [i; 32]).collect(); + for leaf in &leaves { + tree.insert(leaf); + } + (tree, leaves) + } + + /// The oblivious PIR path equals the cleartext tree proof and reconstructs the + /// root, for every leaf of a tree with odd-promoted levels (5 leaves). + #[test] + fn pir_path_matches_cleartext_and_reconstructs_root() { + let (tree, leaves) = tree_with(5); + let root = B256::from_slice(&tree.root().unwrap()); + let client = SimplePirClient::from_tree(&tree); + + for i in 0..leaves.len() as u64 { + let pir = client.fetch_membership_path(i).expect("pir path"); + let reference = tree.generate_commitment_proof(i).expect("cleartext path"); + assert_eq!(pir.path, reference.path, "siblings must match cleartext (leaf {i})"); + assert_eq!(pir.indices, reference.indices, "index bits must match cleartext (leaf {i})"); + assert_eq!( + pir.reconstruct_root(B256::from_slice(&leaves[i as usize])), + root, + "reconstructed root must match (leaf {i})", + ); + } + } + + #[test] + fn single_leaf_has_empty_path() { + let (tree, leaves) = tree_with(1); + let client = SimplePirClient::from_tree(&tree); + let proof = client.fetch_membership_path(0).expect("path"); + assert_eq!(proof.proof_length, 0); + assert_eq!(proof.reconstruct_root(B256::from_slice(&leaves[0])), B256::from_slice(&tree.root().unwrap())); + } + + #[test] + fn empty_tree_and_out_of_range_error() { + let empty = SimplePirClient::from_tree(&CommitmentTree::new()); + assert_eq!(empty.fetch_membership_path(0), Err(PirError::EmptyTree)); + + let (tree, _) = tree_with(3); + let client = SimplePirClient::from_tree(&tree); + assert_eq!(client.fetch_membership_path(3), Err(PirError::LeafOutOfRange(3))); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/adapters/state_replica.rs b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/state_replica.rs new file mode 100644 index 0000000..4818ccb --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/adapters/state_replica.rs @@ -0,0 +1,267 @@ +//! In-memory state-replica core. +//! +//! Replays `ShieldedPoolExt` events to rebuild the public off-chain state the +//! wallet and relayer query: the commitment LeanIMT, the current epoch's active +//! nullifier tree, and one frozen nullifier tree per past epoch. Untrusted for +//! correctness — served witnesses are re-checked in-circuit against +//! light-client-verified roots. +//! +//! Driven by a synthetic event stream in unit tests and by the live RPC event +//! source in the on-chain e2e. The wallet and relayer share one in-process +//! instance (the e2e is in-process; there is no separate HTTP server). + +use std::collections::HashMap; + +use alloy::primitives::B256; + +use crate::{ + adapters::{ + indexed_merkle_tree::{ + IndexedMerkleTree, + IndexedTreeError, + }, + merkle_tree::CommitmentTree, + }, + domain::{ + indexed_merkle::{ + InsertionWitness, + NonMembershipWitness, + }, + merkle::CommitmentMerkleProof, + }, + ports::state_replica::{ + Event, + StateReplicaQuery, + }, +}; + +/// Relayer inputs for the insertion proof: advancing the active tree from +/// `pre_root` to `post_root` by inserting `nullifiers` at canonical indices +/// starting at `pre_leaf_count`. Built against a snapshot of the active tree — +/// the canonical tree advances via the Transfer/Withdraw event ingest when the +/// spend's tx lands, so the relayer can build the proof before the tx confirms. +#[derive(Debug, Clone)] +pub struct InsertionInput { + pub pre_root: B256, + pub post_root: B256, + pub pre_leaf_count: u64, + pub nullifiers: Vec, + pub witnesses: Vec, +} + +/// In-memory replica of public `ShieldedPoolExt` state, rebuilt from events. +pub struct StateReplica { + commitment_tree: CommitmentTree, + active_tree: IndexedMerkleTree, + frozen_trees: HashMap, + current_epoch: u64, +} + +impl Default for StateReplica { + fn default() -> Self { + Self::new() + } +} + +impl StateReplica { + pub fn new() -> Self { + Self { + commitment_tree: CommitmentTree::new(), + active_tree: IndexedMerkleTree::new(), + frozen_trees: HashMap::new(), + current_epoch: 0, + } + } + + /// Apply one event. Events MUST be replayed in block-then-input order so the + /// reconstructed trees match the canonical on-chain ones. + pub fn ingest(&mut self, event: Event) { + match event { + Event::Deposit { commitment } => { + self.commitment_tree.insert(&commitment.0); + } + Event::Transfer { + nullifiers, + output_commitments, + } => { + for commitment in output_commitments { + self.commitment_tree.insert(&commitment.0); + } + for nullifier in nullifiers { + // A well-formed stream never replays a duplicate; ignore the + // Result (a duplicate would indicate an invalid on-chain spend + // that could not have landed). + let _ = self.active_tree.insert(nullifier); + } + } + Event::Withdraw { nullifiers } => { + for nullifier in nullifiers { + let _ = self.active_tree.insert(nullifier); + } + } + Event::EpochRollover { epoch } => { + let frozen = + std::mem::replace(&mut self.active_tree, IndexedMerkleTree::new()); + self.frozen_trees.insert(epoch, frozen); + self.current_epoch = epoch + 1; + } + } + } + + /// Current epoch (number of rollovers ingested). + pub fn current_epoch(&self) -> u64 { + self.current_epoch + } + + /// Number of commitments in the commitment tree. + pub fn commitment_count(&self) -> usize { + self.commitment_tree.len() + } + + /// Current commitment-tree root, or `None` if no commitments yet. + pub fn commitment_root(&self) -> Option { + self.commitment_tree.root().map(B256::from) + } + + /// Current active nullifier-tree root. + pub fn active_nullifier_root(&self) -> B256 { + self.active_tree.root() + } + + /// Membership path for an owned commitment leaf (served cleartext by the + /// `CleartextPirClient` dev stub; obliviously by the real `SimplePirClient`). + pub fn commitment_proof(&self, leaf_index: u64) -> Option { + self.commitment_tree.generate_commitment_proof(leaf_index) + } + + /// Relayer: build the insertion-proof inputs for a spend's `nullifiers`, + /// against a snapshot of the current active tree. Does NOT mutate the + /// canonical tree — that advances via the Transfer/Withdraw event ingest when + /// the spend lands — so the relayer can build the proof before the tx confirms. + pub fn build_insertion( + &self, + nullifiers: &[B256], + ) -> Result { + let mut tree = self.active_tree.clone(); + let pre_root = tree.root(); + let pre_leaf_count = tree.leaf_count(); + let mut witnesses = Vec::with_capacity(nullifiers.len()); + for &nullifier in nullifiers { + witnesses.push(tree.insert(nullifier)?); + } + Ok(InsertionInput { + pre_root, + post_root: tree.root(), + pre_leaf_count, + nullifiers: nullifiers.to_vec(), + witnesses, + }) + } +} + +impl StateReplicaQuery for StateReplica { + fn phantom_witness(&self, epoch: u64, nullifier: B256) -> Option { + self.frozen_trees.get(&epoch)?.non_membership_witness(nullifier) + } + + fn frozen_nullifier_root(&self, epoch: u64) -> Option { + self.frozen_trees.get(&epoch).map(IndexedMerkleTree::root) + } +} + +#[cfg(test)] +mod tests { + use std::sync::{ + Arc, + RwLock, + }; + + use alloy::primitives::{ + B256, + U256, + }; + + use super::*; + use crate::{ + adapters::pir_client::CleartextPirClient, + ports::pir::PirClient, + }; + + fn b(n: u64) -> B256 { + B256::from(U256::from(n)) + } + + #[test] + fn relayer_insertion_witnesses_chain_to_post_root() { + let replica = StateReplica::new(); + let nullifiers = [b(50), b(90)]; + + let input = replica.build_insertion(&nullifiers).unwrap(); + + // build_insertion uses a snapshot: the canonical active tree is untouched. + assert_eq!(replica.active_nullifier_root(), input.pre_root); + assert_eq!(input.pre_leaf_count, 1, "genesis leaf occupies index 0"); + + // Each witness advances the root (the same chain the insertion circuit + // verifies), reaching post_root. + let mut root = input.pre_root; + for (i, witness) in input.witnesses.iter().enumerate() { + root = witness + .verify_and_apply(root, input.nullifiers[i]) + .expect("valid insertion"); + } + assert_eq!(root, input.post_root); + } + + #[test] + fn replays_stream_and_serves_consistent_witnesses() { + let replica = Arc::new(RwLock::new(StateReplica::new())); + + let (c0, c1, c2) = (b(111), b(222), b(333)); + let n1 = b(40); // nullifier spent in epoch 0 + + { + let mut r = replica.write().unwrap(); + r.ingest(Event::Deposit { commitment: c0 }); + r.ingest(Event::Transfer { + nullifiers: vec![n1], + output_commitments: vec![c1, c2], + }); + r.ingest(Event::EpochRollover { epoch: 0 }); + } + + // Commitment tree holds c0, c1, c2; the PIR'd path for leaf 0 reconstructs + // the live commitment root. + let pir = CleartextPirClient::new(replica.clone()); + let proof = pir.fetch_membership_path(0).unwrap(); + + let r = replica.read().unwrap(); + assert_eq!(r.commitment_count(), 3); + assert_eq!(r.current_epoch(), 1); + assert_eq!( + proof.reconstruct_root(c0), + r.commitment_root().unwrap(), + "PIR'd path must rebuild the commitment root" + ); + + // Phantom non-membership for an absent nullifier in frozen epoch 0 verifies + // against that epoch's frozen root. + let absent = b(99); + let witness = r.phantom_witness(0, absent).unwrap(); + assert!(witness.verify(r.frozen_nullifier_root(0).unwrap(), absent)); + + // n1 was spent in epoch 0, so it is present — no phantom witness. + assert!(r.phantom_witness(0, n1).is_none()); + + // Active tree was reset on rollover. + let fresh = IndexedMerkleTree::new(); + assert_eq!(r.active_nullifier_root(), fresh.root()); + } + + #[test] + fn pir_fetch_on_empty_tree_errors() { + let replica = Arc::new(RwLock::new(StateReplica::new())); + let pir = CleartextPirClient::new(replica.clone()); + assert!(pir.fetch_membership_path(0).is_err()); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/crypto.rs b/pocs/private-payment/shielded-pool-extension/src/lib/crypto.rs new file mode 100644 index 0000000..9ab3e11 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/crypto.rs @@ -0,0 +1,2 @@ +pub mod encryption; +pub mod poseidon; diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/crypto/encryption.rs b/pocs/private-payment/shielded-pool-extension/src/lib/crypto/encryption.rs new file mode 100644 index 0000000..3c74869 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/crypto/encryption.rs @@ -0,0 +1,225 @@ +use chacha20poly1305::{ + ChaCha20Poly1305, + Nonce, + aead::{ + Aead, + KeyInit, + }, +}; +use hkdf::Hkdf; +use k256::{ + PublicKey, + ecdh::EphemeralSecret, + elliptic_curve::sec1::ToEncodedPoint, +}; +use sha2::{ + Digest, + Sha256, +}; + +use crate::domain::{ + encrypted::EncryptedNote, + keys::{ + ViewingKey, + ViewingPubkey, + }, + note::Note, +}; + +/// Domain separator for HKDF key derivation. +const HKDF_INFO: &[u8] = b"shielded-pool-note-encryption-v1"; + +/// Encrypt a note for a recipient using ECIES. +/// +/// Scheme: +/// 1. Generate ephemeral keypair +/// 2. ECDH: shared_secret = ephemeral_secret * recipient_pubkey +/// 3. HKDF: derive symmetric key from shared_secret +/// 4. ChaCha20-Poly1305: encrypt note with derived key +pub fn encrypt_note(note: &Note, recipient: &ViewingPubkey) -> EncryptedNote { + // Serialize the note + let plaintext = serde_json::to_vec(note).expect("Note serialization should not fail"); + + // Generate ephemeral keypair + let ephemeral_secret = EphemeralSecret::random(&mut rand::thread_rng()); + let ephemeral_pubkey = ephemeral_secret.public_key(); + + // ECDH: compute shared secret + let shared_secret = ephemeral_secret.diffie_hellman(recipient.public_key()); + + // HKDF: derive encryption key + let hkdf = Hkdf::::new(None, shared_secret.raw_secret_bytes().as_slice()); + let mut encryption_key = [0u8; 32]; + hkdf.expand(HKDF_INFO, &mut encryption_key) + .expect("HKDF expand should not fail with 32 byte output"); + + // ChaCha20-Poly1305 encrypt + // Use first 12 bytes of shared secret hash as nonce (deterministic for same ephemeral key) + let mut nonce_bytes = [0u8; 12]; + let nonce_material = Sha256::digest(shared_secret.raw_secret_bytes().as_slice()); + nonce_bytes.copy_from_slice(&nonce_material[..12]); + let nonce = Nonce::from_slice(&nonce_bytes); + + let cipher = ChaCha20Poly1305::new_from_slice(&encryption_key) + .expect("ChaCha20Poly1305 key should be valid"); + let ciphertext = cipher + .encrypt(nonce, plaintext.as_slice()) + .expect("Encryption should not fail"); + + // Serialize ephemeral public key (compressed SEC1) + let ephemeral_pubkey_bytes = + ephemeral_pubkey.to_encoded_point(true).as_bytes().to_vec(); + + EncryptedNote::new(ephemeral_pubkey_bytes, ciphertext) +} + +/// Decrypt a note using the recipient's viewing key. +/// +/// Scheme: +/// 1. Parse ephemeral public key +/// 2. ECDH: shared_secret = viewing_key * ephemeral_pubkey +/// 3. HKDF: derive symmetric key from shared_secret +/// 4. ChaCha20-Poly1305: decrypt ciphertext +pub fn decrypt_note( + encrypted: &EncryptedNote, + viewing_key: &ViewingKey, +) -> Result { + // Parse ephemeral public key + let ephemeral_pubkey = PublicKey::from_sec1_bytes(&encrypted.ephemeral_pubkey) + .map_err(|_| DecryptionError::InvalidEphemeralKey)?; + + // ECDH: compute shared secret + let shared_secret = k256::ecdh::diffie_hellman( + viewing_key.secret_key().to_nonzero_scalar(), + ephemeral_pubkey.as_affine(), + ); + + // HKDF: derive encryption key + let hkdf = Hkdf::::new(None, shared_secret.raw_secret_bytes().as_slice()); + let mut encryption_key = [0u8; 32]; + hkdf.expand(HKDF_INFO, &mut encryption_key) + .map_err(|_| DecryptionError::KeyDerivationFailed)?; + + // Derive nonce (same as encryption) + let mut nonce_bytes = [0u8; 12]; + let nonce_material = Sha256::digest(shared_secret.raw_secret_bytes().as_slice()); + nonce_bytes.copy_from_slice(&nonce_material[..12]); + let nonce = Nonce::from_slice(&nonce_bytes); + + // ChaCha20-Poly1305 decrypt + let cipher = ChaCha20Poly1305::new_from_slice(&encryption_key) + .map_err(|_| DecryptionError::CipherInitFailed)?; + let plaintext = cipher + .decrypt(nonce, encrypted.ciphertext.as_slice()) + .map_err(|_| DecryptionError::DecryptionFailed)?; + + // Deserialize note + serde_json::from_slice(&plaintext).map_err(|_| DecryptionError::DeserializationFailed) +} + +/// Errors that can occur during decryption. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum DecryptionError { + #[error("Invalid ephemeral public key")] + InvalidEphemeralKey, + #[error("Key derivation failed")] + KeyDerivationFailed, + #[error("Cipher initialization failed")] + CipherInitFailed, + #[error("Decryption failed (wrong key or corrupted data)")] + DecryptionFailed, + #[error("Failed to deserialize note")] + DeserializationFailed, +} + +#[cfg(test)] +mod tests { + use alloy::primitives::{ + Address, + U256, + }; + + use super::*; + use crate::domain::{ + epoch::Epoch, + keys::SpendingKey, + }; + + fn create_test_note() -> Note { + let sk = SpendingKey::random(); + let owner = sk.derive_owner_pubkey(); + Note::new(Address::ZERO, U256::from(1000u64), owner, Epoch(7)) + } + + #[test] + fn test_encrypt_decrypt_roundtrip() { + let note = create_test_note(); + let viewing_key = ViewingKey::random(); + let viewing_pubkey = viewing_key.derive_viewing_pubkey(); + + let encrypted = encrypt_note(¬e, &viewing_pubkey); + let decrypted = decrypt_note(&encrypted, &viewing_key).unwrap(); + + assert_eq!(note.token, decrypted.token); + assert_eq!(note.amount, decrypted.amount); + assert_eq!(note.owner_pubkey, decrypted.owner_pubkey); + assert_eq!(note.salt, decrypted.salt); + assert_eq!(note.epoch_created, decrypted.epoch_created); + } + + #[test] + fn test_decrypt_with_wrong_key_fails() { + let note = create_test_note(); + let viewing_key = ViewingKey::random(); + let viewing_pubkey = viewing_key.derive_viewing_pubkey(); + + let encrypted = encrypt_note(¬e, &viewing_pubkey); + + // Try to decrypt with a different key + let wrong_key = ViewingKey::random(); + let result = decrypt_note(&encrypted, &wrong_key); + + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), DecryptionError::DecryptionFailed); + } + + #[test] + fn test_encrypt_produces_different_ciphertext() { + let note = create_test_note(); + let viewing_key = ViewingKey::random(); + let viewing_pubkey = viewing_key.derive_viewing_pubkey(); + + // Encrypt twice - should produce different ciphertexts due to random ephemeral keys + let encrypted1 = encrypt_note(¬e, &viewing_pubkey); + let encrypted2 = encrypt_note(¬e, &viewing_pubkey); + + assert_ne!(encrypted1.ephemeral_pubkey, encrypted2.ephemeral_pubkey); + assert_ne!(encrypted1.ciphertext, encrypted2.ciphertext); + + // But both should decrypt to the same note + let decrypted1 = decrypt_note(&encrypted1, &viewing_key).unwrap(); + let decrypted2 = decrypt_note(&encrypted2, &viewing_key).unwrap(); + + assert_eq!(decrypted1.token, decrypted2.token); + assert_eq!(decrypted1.amount, decrypted2.amount); + assert_eq!(decrypted1.salt, decrypted2.salt); + assert_eq!(decrypted1.epoch_created, decrypted2.epoch_created); + } + + #[test] + fn test_tampered_ciphertext_fails() { + let note = create_test_note(); + let viewing_key = ViewingKey::random(); + let viewing_pubkey = viewing_key.derive_viewing_pubkey(); + + let mut encrypted = encrypt_note(¬e, &viewing_pubkey); + + // Tamper with ciphertext + if let Some(byte) = encrypted.ciphertext.get_mut(0) { + *byte ^= 0xFF; + } + + let result = decrypt_note(&encrypted, &viewing_key); + assert!(result.is_err()); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/crypto/poseidon.rs b/pocs/private-payment/shielded-pool-extension/src/lib/crypto/poseidon.rs new file mode 100644 index 0000000..fc9b1fb --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/crypto/poseidon.rs @@ -0,0 +1,190 @@ +use alloy::primitives::B256; +use ark_bn254::Fr; +use ark_ff::{ + BigInteger, + PrimeField, +}; +use light_poseidon::{ + Poseidon, + PoseidonHasher, +}; + +/// Convert B256 to BN254 field element. +fn b256_to_fr(value: B256) -> Fr { + Fr::from_be_bytes_mod_order(value.as_ref()) +} + +/// Convert BN254 field element to B256. +fn fr_to_b256(value: Fr) -> B256 { + let big_int = value.into_bigint(); + let bytes = big_int.to_bytes_be(); + B256::from_slice(&bytes) +} + +// NOTE on cross-implementation consistency: these `new_circom(n)` parameters +// MUST match the Noir `poseidon::bn254::hash_n` parameters for every arity, or +// off-chain commitments/nullifiers won't match the in-circuit reconstruction. +// The parent relies on this for arities 2 and 4; this extension additionally +// relies on arities 3 (per-epoch nullifier) and 5 (extended commitment). The +// deposit-circuit checkpoint verifies the new arities against Noir. + +/// Poseidon hash with 1 input (for key derivation). +/// Used for: owner_pubkey = poseidon1(spending_key) +pub fn poseidon1(a: B256) -> B256 { + let mut hasher = + Poseidon::::new_circom(1).expect("Failed to create Poseidon hasher"); + let input = b256_to_fr(a); + let result = hasher + .hash(&[input]) + .expect("Failed to compute Poseidon hash"); + fr_to_b256(result) +} + +/// Poseidon hash with 2 inputs (Merkle inner nodes, chain-proof accumulator fold). +/// Used for: +/// - merkle_node = poseidon2(left, right) +/// - accumulator = poseidon2(accumulator_prev, frozen_root) +pub fn poseidon2(a: B256, b: B256) -> B256 { + let mut hasher = + Poseidon::::new_circom(2).expect("Failed to create Poseidon hasher"); + let inputs = [b256_to_fr(a), b256_to_fr(b)]; + let result = hasher + .hash(&inputs) + .expect("Failed to compute Poseidon hash"); + fr_to_b256(result) +} + +/// Poseidon hash with 3 inputs (per-epoch nullifiers). +/// Used for: η_e = poseidon3(commitment, spending_key, epoch_id) +pub fn poseidon3(a: B256, b: B256, c: B256) -> B256 { + let mut hasher = + Poseidon::::new_circom(3).expect("Failed to create Poseidon hasher"); + let inputs = [b256_to_fr(a), b256_to_fr(b), b256_to_fr(c)]; + let result = hasher + .hash(&inputs) + .expect("Failed to compute Poseidon hash"); + fr_to_b256(result) +} + +/// Poseidon hash with 4 inputs (attestation leaves). +/// Used for: attestation_leaf = poseidon4(subject_pubkey, attester, issued_at, expires_at) +pub fn poseidon4(a: B256, b: B256, c: B256, d: B256) -> B256 { + let mut hasher = + Poseidon::::new_circom(4).expect("Failed to create Poseidon hasher"); + let inputs = [b256_to_fr(a), b256_to_fr(b), b256_to_fr(c), b256_to_fr(d)]; + let result = hasher + .hash(&inputs) + .expect("Failed to compute Poseidon hash"); + fr_to_b256(result) +} + +/// Poseidon hash with 5 inputs (extended note commitments). +/// Used for: commitment = poseidon5(token, amount, owner_pubkey, salt, epoch_created) +pub fn poseidon5(a: B256, b: B256, c: B256, d: B256, e: B256) -> B256 { + let mut hasher = + Poseidon::::new_circom(5).expect("Failed to create Poseidon hasher"); + let inputs = [ + b256_to_fr(a), + b256_to_fr(b), + b256_to_fr(c), + b256_to_fr(d), + b256_to_fr(e), + ]; + let result = hasher + .hash(&inputs) + .expect("Failed to compute Poseidon hash"); + fr_to_b256(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_poseidon1_deterministic() { + let input = B256::repeat_byte(0x42); + assert_eq!(poseidon1(input), poseidon1(input)); + } + + #[test] + fn test_poseidon2_order_matters() { + let a = B256::repeat_byte(0x01); + let b = B256::repeat_byte(0x02); + assert_ne!(poseidon2(a, b), poseidon2(b, a)); + } + + #[test] + fn test_poseidon3_deterministic() { + let a = B256::repeat_byte(0x01); + let b = B256::repeat_byte(0x02); + let c = B256::repeat_byte(0x03); + assert_eq!(poseidon3(a, b, c), poseidon3(a, b, c)); + } + + #[test] + fn test_poseidon3_epoch_separation() { + // Same (commitment, key), different epoch -> different nullifier. + let commitment = B256::repeat_byte(0x42); + let key = B256::repeat_byte(0x07); + let e0 = B256::ZERO; + let e1 = B256::from(alloy::primitives::U256::from(1u64)); + assert_ne!(poseidon3(commitment, key, e0), poseidon3(commitment, key, e1)); + } + + #[test] + fn test_poseidon5_deterministic() { + let a = B256::repeat_byte(0x01); + let b = B256::repeat_byte(0x02); + let c = B256::repeat_byte(0x03); + let d = B256::repeat_byte(0x04); + let e = B256::repeat_byte(0x05); + assert_eq!(poseidon5(a, b, c, d, e), poseidon5(a, b, c, d, e)); + } + + #[test] + fn test_poseidon5_epoch_separation() { + // Same note fields, different epoch_created -> different commitment. + let token = B256::repeat_byte(0x11); + let amount = B256::repeat_byte(0x22); + let owner = B256::repeat_byte(0x33); + let salt = B256::repeat_byte(0x44); + let e0 = B256::ZERO; + let e1 = B256::from(alloy::primitives::U256::from(1u64)); + assert_ne!( + poseidon5(token, amount, owner, salt, e0), + poseidon5(token, amount, owner, salt, e1) + ); + } + + // Cross-implementation vectors. These MUST equal the Noir + // `poseidon::bn254::hash_n` outputs for the same inputs, or off-chain + // commitments/nullifiers diverge from the in-circuit reconstruction. The + // deposit circuit pins POSEIDON5_12345 against Noir + // (`test_poseidon5_matches_rust`); the chain-update/spend circuits pin + // POSEIDON3_123 likewise. If a vector here changes (e.g. a poseidon lib + // bump), the matching Noir test MUST be updated in lockstep. + const POSEIDON3_123: &str = + "0x0e7732d89e6939c0ff03d5e58dab6302f3230e269dc5b968f725df34ab36d732"; + const POSEIDON5_12345: &str = + "0x0dab9449e4a1398a15224c0b15a49d598b2174d305a316c918125f8feeb123c0"; + + fn small(n: u64) -> B256 { + B256::from(alloy::primitives::U256::from(n)) + } + + #[test] + fn test_poseidon3_known_vector() { + assert_eq!( + poseidon3(small(1), small(2), small(3)), + POSEIDON3_123.parse::().unwrap() + ); + } + + #[test] + fn test_poseidon5_known_vector() { + assert_eq!( + poseidon5(small(1), small(2), small(3), small(4), small(5)), + POSEIDON5_12345.parse::().unwrap() + ); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain.rs new file mode 100644 index 0000000..d66e59b --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain.rs @@ -0,0 +1,11 @@ +pub mod chain_proof; +pub mod commitment; +pub mod encrypted; +pub mod epoch; +pub mod indexed_merkle; +pub mod keys; +pub mod merkle; +pub mod note; +pub mod nullifier; +pub mod proof; +pub mod witness; diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain/chain_proof.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain/chain_proof.rs new file mode 100644 index 0000000..d7739c5 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain/chain_proof.rs @@ -0,0 +1,123 @@ +//! Per-note IVC chain-proof state. +//! +//! Mirrors the public inputs of the chain-update circuit (see +//! `circuits/chain_update`): a [`ChainProof`] attests that a note is unspent +//! from `epoch_created` through `epoch_validated_through - 1`, with the folded +//! frozen roots committed in `accumulator`. The wallet maintains one per owned +//! note, extending it one frozen epoch per rollover. + +use alloy::primitives::B256; +use serde::{ + Deserialize, + Serialize, +}; + +use crate::{ + crypto::poseidon::poseidon2, + domain::{ + commitment::Commitment, + epoch::Epoch, + }, +}; + +/// Chain-proof state (the chain-update circuit's logical public inputs, minus +/// the recursion's `fixed_vk_hash`, which is a prover-side artifact). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct ChainProof { + /// The note this chain attests about. + pub commitment: Commitment, + /// Epoch the note was created in (bound into the commitment). + pub epoch_created: Epoch, + /// Next epoch needing non-membership; equals `currentEpoch` when fully + /// caught up (`epoch_created <= epoch_validated_through <= currentEpoch`). + pub epoch_validated_through: Epoch, + /// Running Poseidon hash over the frozen roots folded so far. + pub accumulator: B256, +} + +impl ChainProof { + /// Genesis (base case): nothing folded yet. + pub fn genesis(commitment: Commitment, epoch_created: Epoch) -> Self { + Self { + commitment, + epoch_created, + epoch_validated_through: epoch_created, + accumulator: B256::ZERO, + } + } + + /// Whether this is the genesis (base-case) state. + pub fn is_genesis(&self) -> bool { + self.epoch_validated_through == self.epoch_created && self.accumulator == B256::ZERO + } + + /// Fold the frozen root of `epoch_validated_through` into the chain, + /// advancing one epoch: `accumulator' = poseidon2(accumulator, frozen_root)`. + pub fn extend(&self, frozen_root: B256) -> Self { + Self { + commitment: self.commitment, + epoch_created: self.epoch_created, + epoch_validated_through: self.epoch_validated_through.next(), + accumulator: poseidon2(self.accumulator, frozen_root), + } + } +} + +/// The accumulator the on-chain `expectedChainAccumulator` recomputes: a +/// sequential `poseidon2` fold of `frozen_roots` starting from zero. A genesis +/// chain proof extended over these same roots reaches this value. +pub fn expected_accumulator(frozen_roots: &[B256]) -> B256 { + frozen_roots + .iter() + .fold(B256::ZERO, |acc, root| poseidon2(acc, *root)) +} + +#[cfg(test)] +mod tests { + use alloy::primitives::U256; + + use super::*; + + fn c(n: u64) -> Commitment { + Commitment(B256::from(U256::from(n))) + } + + fn r(n: u64) -> B256 { + B256::from(U256::from(n)) + } + + #[test] + fn genesis_is_empty() { + let g = ChainProof::genesis(c(42), Epoch(3)); + assert!(g.is_genesis()); + assert_eq!(g.epoch_validated_through, Epoch(3)); + assert_eq!(g.accumulator, B256::ZERO); + } + + #[test] + fn extend_advances_epoch_and_folds() { + let g = ChainProof::genesis(c(42), Epoch(0)); + let s1 = g.extend(r(10)); + assert_eq!(s1.epoch_validated_through, Epoch(1)); + assert_eq!(s1.accumulator, poseidon2(B256::ZERO, r(10))); + assert!(!s1.is_genesis()); + + let s2 = s1.extend(r(20)); + assert_eq!(s2.epoch_validated_through, Epoch(2)); + assert_eq!(s2.accumulator, poseidon2(poseidon2(B256::ZERO, r(10)), r(20))); + // commitment / epoch_created are invariant across extension. + assert_eq!(s2.commitment, c(42)); + assert_eq!(s2.epoch_created, Epoch(0)); + } + + #[test] + fn extension_matches_on_chain_expected_accumulator() { + let roots = [r(10), r(20), r(30)]; + let mut proof = ChainProof::genesis(c(7), Epoch(0)); + for root in roots { + proof = proof.extend(root); + } + assert_eq!(proof.accumulator, expected_accumulator(&roots)); + assert_eq!(proof.epoch_validated_through, Epoch(3)); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain/commitment.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain/commitment.rs new file mode 100644 index 0000000..2c6f371 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain/commitment.rs @@ -0,0 +1,101 @@ +use alloy::primitives::B256; +use serde::{ + Deserialize, + Serialize, +}; + +use super::keys::SpendingKey; +use crate::{ + crypto::poseidon::poseidon3, + domain::{ + epoch::Epoch, + nullifier::Nullifier, + }, +}; + +/// A commitment is the on-chain representation of a note. +/// It hides all note contents while allowing proof of ownership. +/// +/// Extended vs parent: the preimage now binds `epoch_created`, so +/// `commitment = poseidon5(token, amount, owner_pubkey, salt, epoch_created)` +/// (computed by [`crate::domain::note::Note::commitment`]). Binding the epoch +/// lets the verifier enforce that a note's chain proof covers its full lifetime. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Commitment(pub B256); + +impl Commitment { + /// Create a commitment from raw bytes. + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(B256::from(bytes)) + } + + /// Get the raw bytes. + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_ref() + } + + /// Compute the per-epoch nullifier for this commitment. + /// + /// `η_e = poseidon3(commitment, spending_key, epoch_id)` + /// + /// `epoch` is the epoch the nullifier is derived for, not necessarily the + /// note's `epoch_created`: the chain proof derives phantom nullifiers for + /// each past epoch `[epoch_created, currentEpoch - 1]`, and the spend + /// derives the active nullifier for `currentEpoch`. + pub fn nullifier(&self, spending_key: &SpendingKey, epoch: Epoch) -> Nullifier { + let hash = poseidon3(self.0, spending_key.0, epoch.as_field()); + Nullifier(hash) + } +} + +impl From for Commitment { + fn from(value: B256) -> Self { + Self(value) + } +} + +impl From for B256 { + fn from(value: Commitment) -> Self { + value.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_commitment_nullifier_deterministic() { + let commitment = Commitment(B256::repeat_byte(0x42)); + let sk = SpendingKey::from_bytes([0x01; 32]); + + let n1 = commitment.nullifier(&sk, Epoch(3)); + let n2 = commitment.nullifier(&sk, Epoch(3)); + + assert_eq!(n1, n2); + } + + #[test] + fn test_commitment_nullifier_different_keys() { + let commitment = Commitment(B256::repeat_byte(0x42)); + let sk1 = SpendingKey::from_bytes([0x01; 32]); + let sk2 = SpendingKey::from_bytes([0x02; 32]); + + assert_ne!( + commitment.nullifier(&sk1, Epoch(0)), + commitment.nullifier(&sk2, Epoch(0)) + ); + } + + #[test] + fn test_commitment_nullifier_different_epochs() { + // The same note produces a distinct nullifier in each epoch. + let commitment = Commitment(B256::repeat_byte(0x42)); + let sk = SpendingKey::from_bytes([0x01; 32]); + + assert_ne!( + commitment.nullifier(&sk, Epoch(0)), + commitment.nullifier(&sk, Epoch(1)) + ); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain/encrypted.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain/encrypted.rs new file mode 100644 index 0000000..cd08e9e --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain/encrypted.rs @@ -0,0 +1,170 @@ +use serde::{ + Deserialize, + Serialize, +}; + +use super::keys::ViewingPubkey; +use crate::domain::commitment::Commitment; + +/// An encrypted note payload using ECIES (secp256k1 + ChaCha20-Poly1305). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EncryptedNote { + /// Ephemeral public key used for ECDH (compressed SEC1 format, 33 bytes) + pub ephemeral_pubkey: Vec, + /// Encrypted note data (ChaCha20-Poly1305 ciphertext + tag) + pub ciphertext: Vec, +} + +impl EncryptedNote { + /// Create from raw components. + pub fn new(ephemeral_pubkey: Vec, ciphertext: Vec) -> Self { + Self { + ephemeral_pubkey, + ciphertext, + } + } + + /// Serialize to bytes for the on-chain event log (the `Deposit`/`Transfer` + /// event carries the encrypted note; it is not written to contract storage). + /// Log cost is linear in ciphertext size; a production deployment would use a + /// compact note format or an off-chain note log with FMD/OMR note-discovery + /// (SPEC "Off-Chain State-Replica Server"). + pub fn to_bytes(&self) -> Vec { + let mut bytes = + Vec::with_capacity(1 + self.ephemeral_pubkey.len() + self.ciphertext.len()); + bytes.push(self.ephemeral_pubkey.len() as u8); + bytes.extend_from_slice(&self.ephemeral_pubkey); + bytes.extend_from_slice(&self.ciphertext); + bytes + } + + /// Deserialize from bytes. + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err("Empty bytes"); + } + + let pubkey_len = bytes[0] as usize; + if bytes.len() < 1 + pubkey_len { + return Err("Invalid encrypted note format"); + } + + let ephemeral_pubkey = bytes[1..1 + pubkey_len].to_vec(); + let ciphertext = bytes[1 + pubkey_len..].to_vec(); + + Ok(Self { + ephemeral_pubkey, + ciphertext, + }) + } +} + +/// A P2P message containing an encrypted note and its commitment. +/// Used for off-chain note delivery between transactors. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct P2pMessage { + /// The encrypted note + pub encrypted_note: EncryptedNote, + /// The commitment (for identifying the note on-chain) + pub commitment: Commitment, + /// The recipient's viewing public key (for routing) + pub recipient_viewing_pubkey: ViewingPubkey, +} + +impl P2pMessage { + /// Create a new P2P message. + pub fn new( + encrypted_note: EncryptedNote, + commitment: Commitment, + recipient_viewing_pubkey: ViewingPubkey, + ) -> Self { + Self { + encrypted_note, + commitment, + recipient_viewing_pubkey, + } + } +} + +/// Encrypted notes payload for on-chain transfer events. +/// Contains two encrypted notes (one for each output). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EncryptedTransferNotes { + /// Encrypted note for output 1 + pub note_1: EncryptedNote, + /// Encrypted note for output 2 + pub note_2: EncryptedNote, +} + +impl EncryptedTransferNotes { + /// Create from two encrypted notes. + pub fn new(note_1: EncryptedNote, note_2: EncryptedNote) -> Self { + Self { note_1, note_2 } + } + + /// Serialize to bytes for the on-chain `Transfer` event log (see + /// [`EncryptedNote::to_bytes`] for the calldata-size note). + pub fn to_bytes(&self) -> Vec { + let bytes_1 = self.note_1.to_bytes(); + let bytes_2 = self.note_2.to_bytes(); + + let mut bytes = Vec::with_capacity(4 + bytes_1.len() + bytes_2.len()); + bytes.extend_from_slice(&(bytes_1.len() as u32).to_be_bytes()); + bytes.extend_from_slice(&bytes_1); + bytes.extend_from_slice(&bytes_2); + bytes + } + + /// Deserialize from bytes. + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < 4 { + return Err("Invalid encrypted transfer notes format"); + } + + let len_1 = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize; + if bytes.len() < 4 + len_1 { + return Err("Invalid encrypted transfer notes format"); + } + + let note_1 = EncryptedNote::from_bytes(&bytes[4..4 + len_1])?; + let note_2 = EncryptedNote::from_bytes(&bytes[4 + len_1..])?; + + Ok(Self { note_1, note_2 }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encrypted_note_roundtrip() { + let note = EncryptedNote::new(vec![0x02; 33], vec![0xAB; 100]); + let bytes = note.to_bytes(); + let recovered = EncryptedNote::from_bytes(&bytes).unwrap(); + + assert_eq!(note.ephemeral_pubkey, recovered.ephemeral_pubkey); + assert_eq!(note.ciphertext, recovered.ciphertext); + } + + #[test] + fn test_encrypted_transfer_notes_roundtrip() { + let note_1 = EncryptedNote::new(vec![0x02; 33], vec![0xAB; 100]); + let note_2 = EncryptedNote::new(vec![0x03; 33], vec![0xCD; 80]); + let notes = EncryptedTransferNotes::new(note_1, note_2); + + let bytes = notes.to_bytes(); + let recovered = EncryptedTransferNotes::from_bytes(&bytes).unwrap(); + + assert_eq!( + notes.note_1.ephemeral_pubkey, + recovered.note_1.ephemeral_pubkey + ); + assert_eq!(notes.note_1.ciphertext, recovered.note_1.ciphertext); + assert_eq!( + notes.note_2.ephemeral_pubkey, + recovered.note_2.ephemeral_pubkey + ); + assert_eq!(notes.note_2.ciphertext, recovered.note_2.ciphertext); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain/epoch.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain/epoch.rs new file mode 100644 index 0000000..c9cc0fe --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain/epoch.rs @@ -0,0 +1,66 @@ +use alloy::primitives::{ + B256, + U256, +}; +use serde::{ + Deserialize, + Serialize, +}; + +/// An epoch counter. `currentEpoch` on-chain is advanced by `rolloverEpoch()`; +/// distinct epoch values yield distinct nullifiers for the same note. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +pub struct Epoch(pub u64); + +impl Epoch { + /// The first epoch; notes minted before any rollover are created here. + pub const GENESIS: Epoch = Epoch(0); + + /// Field encoding used in Poseidon preimages and circuit inputs. + /// + /// This is the integer value of the epoch as a field element, matching the + /// Noir-side `epoch as Field` cast (the same convention the parent uses for + /// `amount` and timestamp fields). + pub fn as_field(self) -> B256 { + B256::from(U256::from(self.0)) + } + + /// The next epoch after a rollover. + pub fn next(self) -> Epoch { + Epoch(self.0 + 1) + } +} + +impl From for Epoch { + fn from(value: u64) -> Self { + Self(value) + } +} + +impl From for u64 { + fn from(value: Epoch) -> Self { + value.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_genesis_field_is_zero() { + assert_eq!(Epoch::GENESIS.as_field(), B256::ZERO); + } + + #[test] + fn test_next_increments() { + assert_eq!(Epoch(4).next(), Epoch(5)); + } + + #[test] + fn test_ordering() { + assert!(Epoch(2) < Epoch(3)); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain/indexed_merkle.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain/indexed_merkle.rs new file mode 100644 index 0000000..72dfa28 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain/indexed_merkle.rs @@ -0,0 +1,243 @@ +//! Indexed Merkle tree primitives (sorted-low-leaf pattern). +//! +//! Used for the active and frozen nullifier trees. Leaves are sorted by value +//! and each carries a `(next_value, next_index)` pointer to the next-larger +//! leaf, forming a linked list in value order. This supports two operations: +//! +//! - **Sorted-low-leaf non-membership**: a value `η` is absent iff there is a +//! `low_leaf` with `low_leaf.value < η < low_leaf.next_value`. Used by the +//! chain-update circuit over frozen trees. +//! - **Insertion**: inserting `η` mutates the predecessor (its pointers repoint +//! to `η`) and writes a new leaf at the next free slot. A valid sorted-low-leaf +//! insertion is itself a non-membership proof of `η` in the prior tree. Used by +//! the insertion circuit over the active tree. +//! +//! This module holds the pure types, hashing, and the witness-verification logic +//! that the circuits mirror; [`crate::adapters::indexed_merkle_tree`] holds the +//! stateful tree that generates the witnesses. The `next_value == 0` sentinel +//! denotes "+infinity" (the current largest leaf); real nullifiers are Poseidon +//! outputs and are never 0. + +use alloy::primitives::{ + B256, + U256, +}; +use serde::{ + Deserialize, + Serialize, +}; + +use crate::crypto::poseidon::{ + poseidon2, + poseidon3, +}; + +/// Tree depth (PoC parameter; capacity `2^DEPTH` leaves per tree). Path length +/// in the circuits equals this, so it MUST match the in-circuit constant. +pub const NULLIFIER_TREE_DEPTH: usize = 32; + +/// A leaf of the indexed Merkle tree. +/// +/// `leaf_hash = poseidon3(value, next_value, next_index)`. The all-zero leaf +/// `(0, 0, 0)` is both the empty-slot value and the genesis low-leaf covering +/// the range `[0, +inf)`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct IndexedLeaf { + pub value: B256, + pub next_value: B256, + pub next_index: u64, +} + +impl IndexedLeaf { + /// The empty / genesis leaf `(0, 0, 0)`. + pub const EMPTY: IndexedLeaf = IndexedLeaf { + value: B256::ZERO, + next_value: B256::ZERO, + next_index: 0, + }; + + /// `poseidon3(value, next_value, next_index)`. + pub fn hash(&self) -> B256 { + poseidon3(self.value, self.next_value, index_to_field(self.next_index)) + } +} + +/// Hash of the empty leaf; also the hash stored in every unwritten slot. +pub fn empty_leaf_hash() -> B256 { + IndexedLeaf::EMPTY.hash() +} + +/// Encode a leaf index as a field element (matches the Noir `index as Field`). +pub fn index_to_field(index: u64) -> B256 { + B256::from(U256::from(index)) +} + +fn as_uint(value: B256) -> U256 { + U256::from_be_bytes(value.0) +} + +/// True if `query` falls strictly inside the range covered by `low`: +/// `low.value < query < low.next_value`, with `next_value == 0` meaning +inf. +pub fn covers(low: &IndexedLeaf, query: B256) -> bool { + let lo = as_uint(low.value); + let q = as_uint(query); + let hi = as_uint(low.next_value); + lo < q && (low.next_value == B256::ZERO || q < hi) +} + +/// Recompute the root implied by a leaf at `index` with the given sibling path. +/// `siblings[level]` is the sibling hash at that level; the path bit is the +/// `level`-th bit of `index` (0 = current node is the left child). +pub fn root_from_path(leaf_hash: B256, index: u64, siblings: &[B256]) -> B256 { + let mut node = leaf_hash; + let mut idx = index; + for sibling in siblings { + node = if idx & 1 == 0 { + poseidon2(node, *sibling) + } else { + poseidon2(*sibling, node) + }; + idx >>= 1; + } + node +} + +/// Verify that `leaf_hash` sits at `index` under `root` via `siblings`. +pub fn verify_inclusion(root: B256, leaf_hash: B256, index: u64, siblings: &[B256]) -> bool { + root_from_path(leaf_hash, index, siblings) == root +} + +/// Sorted-low-leaf non-membership witness for the chain-update circuit. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NonMembershipWitness { + pub low_leaf: IndexedLeaf, + pub low_leaf_index: u64, + /// Sibling path of `low_leaf` (length `NULLIFIER_TREE_DEPTH`). + pub siblings: Vec, +} + +impl NonMembershipWitness { + /// Verify `query` is absent under `root`: the low-leaf is included and its + /// range strictly covers `query`. + pub fn verify(&self, root: B256, query: B256) -> bool { + verify_inclusion(root, self.low_leaf.hash(), self.low_leaf_index, &self.siblings) + && covers(&self.low_leaf, query) + } +} + +/// Insertion witness for the insertion circuit: one sorted-low-leaf insertion of +/// `value` advancing the root from a pre-state to a post-state. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InsertionWitness { + /// Predecessor leaf as it stands in the pre-state tree. + pub low_leaf: IndexedLeaf, + pub low_leaf_index: u64, + /// Sibling path of `low_leaf` against the pre-state root. + pub low_leaf_siblings: Vec, + /// Canonical append index for the new leaf (`pre_leaf_count + i - 1`). + pub new_leaf_index: u64, + /// Sibling path of the (empty) append slot against the intermediate root + /// `r'` produced after the predecessor mutation. + pub new_leaf_siblings: Vec, +} + +impl InsertionWitness { + /// Replay the insertion of `value` from `pre_root`, returning the post-root, + /// or `None` if any check fails. Mirrors the insertion circuit exactly: + /// + /// 1. predecessor membership + `low_leaf.value < value < low_leaf.next_value` + /// 2. mutate predecessor `(value, value→next_value, next_index→new_index)` → `r'` + /// 3. the append slot holds the empty leaf in `r'` + /// 4. write the new leaf `(value, low.next_value, low.next_index)` → post-root + pub fn verify_and_apply(&self, pre_root: B256, value: B256) -> Option { + // 1. Predecessor membership and non-membership of `value`. + if !verify_inclusion( + pre_root, + self.low_leaf.hash(), + self.low_leaf_index, + &self.low_leaf_siblings, + ) { + return None; + } + if !covers(&self.low_leaf, value) { + return None; + } + + // 2. Mutate predecessor to point at the new leaf; recompute r'. + let updated_low = IndexedLeaf { + value: self.low_leaf.value, + next_value: value, + next_index: self.new_leaf_index, + }; + let r_prime = root_from_path( + updated_low.hash(), + self.low_leaf_index, + &self.low_leaf_siblings, + ); + + // 3. The append slot must be empty in r'. + if !verify_inclusion( + r_prime, + empty_leaf_hash(), + self.new_leaf_index, + &self.new_leaf_siblings, + ) { + return None; + } + + // 4. Write the new leaf, inheriting the predecessor's old forward pointer. + let new_leaf = IndexedLeaf { + value, + next_value: self.low_leaf.next_value, + next_index: self.low_leaf.next_index, + }; + Some(root_from_path( + new_leaf.hash(), + self.new_leaf_index, + &self.new_leaf_siblings, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_leaf_is_zero_triple() { + assert_eq!(IndexedLeaf::EMPTY.value, B256::ZERO); + assert_eq!(IndexedLeaf::EMPTY.next_value, B256::ZERO); + assert_eq!(IndexedLeaf::EMPTY.next_index, 0); + } + + #[test] + fn covers_uses_infinity_sentinel() { + let genesis = IndexedLeaf::EMPTY; // (0, 0=inf, 0) + let v = B256::from(U256::from(42u64)); + assert!(covers(&genesis, v)); // 0 < 42 < inf + assert!(!covers(&genesis, B256::ZERO)); // 0 < 0 is false + + let mid = IndexedLeaf { + value: B256::from(U256::from(10u64)), + next_value: B256::from(U256::from(20u64)), + next_index: 5, + }; + assert!(covers(&mid, B256::from(U256::from(15u64)))); + assert!(!covers(&mid, B256::from(U256::from(20u64)))); // not strict at upper + assert!(!covers(&mid, B256::from(U256::from(10u64)))); // not strict at lower + assert!(!covers(&mid, B256::from(U256::from(25u64)))); // above range + } + + #[test] + fn inclusion_roundtrips() { + let leaf = IndexedLeaf { + value: B256::from(U256::from(7u64)), + next_value: B256::ZERO, + next_index: 0, + }; + let siblings = vec![B256::ZERO; NULLIFIER_TREE_DEPTH]; + let root = root_from_path(leaf.hash(), 3, &siblings); + assert!(verify_inclusion(root, leaf.hash(), 3, &siblings)); + assert!(!verify_inclusion(root, leaf.hash(), 2, &siblings)); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain/keys.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain/keys.rs new file mode 100644 index 0000000..5b69ae4 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain/keys.rs @@ -0,0 +1,180 @@ +use alloy::primitives::B256; +use rand::Rng; +use serde::{ + Deserialize, + Serialize, +}; +use zeroize::{ + Zeroize, + ZeroizeOnDrop, +}; + +use crate::crypto::poseidon::poseidon1; + +/// Spending key - used to authorize transfers and derive nullifiers. +/// This is the master secret for spending authority. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SpendingKey(pub B256); + +impl SpendingKey { + /// Generate a random spending key. + pub fn random() -> Self { + let mut rng = rand::thread_rng(); + let mut bytes = [0u8; 32]; + rng.fill(&mut bytes[5..]); + Self(B256::from(bytes)) + } + + /// Create from raw bytes. + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(B256::from(bytes)) + } + + /// Derive the owner public key (spending pubkey) from the spending key. + /// owner_pubkey = poseidon1(spending_key) + pub fn derive_owner_pubkey(&self) -> OwnerPubkey { + OwnerPubkey(poseidon1(self.0)) + } + + /// Get the raw bytes. + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_ref() + } +} + +// The spending key is the master spend secret; wipe its bytes on drop so they do +// not linger in freed memory. +impl Zeroize for SpendingKey { + fn zeroize(&mut self) { + self.0 .0.zeroize(); + } +} + +impl ZeroizeOnDrop for SpendingKey {} + +impl Drop for SpendingKey { + fn drop(&mut self) { + self.zeroize(); + } +} + +/// Owner public key - derived from spending key, used in note commitments. +/// owner_pubkey = poseidon1(spending_key) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct OwnerPubkey(pub B256); + +impl OwnerPubkey { + /// Create from raw bytes. + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(B256::from(bytes)) + } + + /// Get the raw bytes. + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_ref() + } +} + +/// Viewing key - used for decrypting notes and enabling audit access. +/// Separate from spending key for selective disclosure. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ViewingKey(pub k256::SecretKey); + +impl ViewingKey { + /// Generate a random viewing key. + pub fn random() -> Self { + Self(k256::SecretKey::random(&mut rand::thread_rng())) + } + + /// Create from raw bytes. + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(Self(k256::SecretKey::from_slice(bytes)?)) + } + + /// Derive the viewing public key. + pub fn derive_viewing_pubkey(&self) -> ViewingPubkey { + ViewingPubkey(self.0.public_key()) + } + + /// Get the secret key reference. + pub fn secret_key(&self) -> &k256::SecretKey { + &self.0 + } +} + +/// Viewing public key - used for encrypting notes to recipients. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ViewingPubkey(#[serde(with = "viewing_pubkey_serde")] pub k256::PublicKey); + +impl ViewingPubkey { + /// Create from raw compressed bytes (33 bytes). + pub fn from_sec1_bytes(bytes: &[u8]) -> Result { + Ok(Self(k256::PublicKey::from_sec1_bytes(bytes)?)) + } + + /// Get the public key reference. + pub fn public_key(&self) -> &k256::PublicKey { + &self.0 + } + + /// Serialize to compressed SEC1 format (33 bytes). + pub fn to_sec1_bytes(&self) -> Vec { + use k256::elliptic_curve::sec1::ToEncodedPoint; + self.0.to_encoded_point(true).as_bytes().to_vec() + } +} + +mod viewing_pubkey_serde { + use k256::elliptic_curve::sec1::ToEncodedPoint; + use serde::{ + Deserialize, + Deserializer, + Serializer, + }; + + pub fn serialize(key: &k256::PublicKey, serializer: S) -> Result + where + S: Serializer, + { + let bytes = key.to_encoded_point(true); + serializer.serialize_bytes(bytes.as_bytes()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let bytes: Vec = Deserialize::deserialize(deserializer)?; + k256::PublicKey::from_sec1_bytes(&bytes).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_spending_key_derivation() { + let sk = SpendingKey::random(); + let pk1 = sk.derive_owner_pubkey(); + let pk2 = sk.derive_owner_pubkey(); + assert_eq!(pk1, pk2, "Derivation should be deterministic"); + } + + #[test] + fn test_viewing_key_derivation() { + let vk = ViewingKey::random(); + let vpk1 = vk.derive_viewing_pubkey(); + let vpk2 = vk.derive_viewing_pubkey(); + assert_eq!(vpk1, vpk2, "Derivation should be deterministic"); + } + + #[test] + fn test_viewing_pubkey_roundtrip() { + let vk = ViewingKey::random(); + let vpk = vk.derive_viewing_pubkey(); + let bytes = vpk.to_sec1_bytes(); + let vpk2 = ViewingPubkey::from_sec1_bytes(&bytes).unwrap(); + assert_eq!(vpk, vpk2); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain/merkle.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain/merkle.rs new file mode 100644 index 0000000..4669281 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain/merkle.rs @@ -0,0 +1,72 @@ +use alloy::primitives::B256; +use serde::{ + Deserialize, + Serialize, +}; + +use crate::crypto::poseidon::poseidon2; + +/// Maximum depth of the commitment Merkle tree (supports up to 2^32 commitments). +/// LeanIMT uses dynamic depth, but arrays are sized to this maximum. +pub const MAX_COMMITMENT_TREE_DEPTH: usize = 32; + +/// Merkle proof for a commitment in the commitment tree. +/// +/// Ported from the parent shielded-pool (LeanIMT membership). Unchanged by this +/// extension: the commitment tree stores commitment hashes opaquely, so binding +/// `epoch_created` into the commitment does not affect the tree structure. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommitmentMerkleProof { + /// Sibling hashes along the path from leaf to root. + pub path: Vec, + /// Index bits indicating left (0) or right (1) at each level. + pub indices: Vec, + /// The leaf index in the tree. + pub leaf_index: u64, + /// Actual proof length (dynamic tree depth at proof-generation time). + pub proof_length: usize, +} + +impl CommitmentMerkleProof { + /// Create a new commitment Merkle proof (proof length inferred from `path`). + pub fn new(path: Vec, indices: Vec, leaf_index: u64) -> Self { + let proof_length = path.len(); + Self { + path, + indices, + leaf_index, + proof_length, + } + } + + /// Reconstruct the tree root from `leaf` and this proof's siblings/indices + /// (`indices[i] == 0` means the running node is the left child at level `i`). + /// Mirrors the circuit's `binary_merkle_root` check. + pub fn reconstruct_root(&self, leaf: B256) -> B256 { + let mut node = leaf; + for (sibling, &bit) in self.path.iter().zip(self.indices.iter()) { + node = if bit == 0 { + poseidon2(node, *sibling) + } else { + poseidon2(*sibling, node) + }; + } + node + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_proof_length_inferred() { + let proof = CommitmentMerkleProof::new( + vec![B256::repeat_byte(1), B256::repeat_byte(2)], + vec![0, 1], + 2, + ); + assert_eq!(proof.proof_length, 2); + assert_eq!(proof.leaf_index, 2); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain/note.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain/note.rs new file mode 100644 index 0000000..2cd28fe --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain/note.rs @@ -0,0 +1,184 @@ +use alloy::primitives::{ + Address, + B256, + U256, +}; +use rand::Rng; +use serde::{ + Deserialize, + Serialize, +}; + +use super::{ + commitment::Commitment, + epoch::Epoch, + keys::{ + OwnerPubkey, + SpendingKey, + }, + nullifier::Nullifier, +}; +use crate::crypto::poseidon::poseidon5; + +/// A note represents a private balance owned by a spending key. +/// Notes are the UTXO primitive in the shielded pool. +/// +/// Extended vs parent: carries `epoch_created`, the value of `currentEpoch` +/// when the note was committed, bound into the commitment. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Note { + /// ERC-20 token contract address + pub token: Address, + /// Token amount (raw units, no decimals) + pub amount: U256, + /// Spending public key of the owner + pub owner_pubkey: OwnerPubkey, + /// Random salt for hiding (prevents commitment collisions) + pub salt: B256, + /// Value of `currentEpoch` at the moment this note was committed + pub epoch_created: Epoch, +} + +impl Note { + /// Create a new note with a random salt. + pub fn new( + token: Address, + amount: U256, + owner_pubkey: OwnerPubkey, + epoch_created: Epoch, + ) -> Self { + let mut rng = rand::thread_rng(); + let mut salt_bytes = [0u8; 32]; + rng.fill(&mut salt_bytes[5..]); // 27 random bytes (top 5 zeroed) stay < BN254 modulus; no rejection sampling + + Self { + token, + amount, + owner_pubkey, + salt: B256::from(salt_bytes), + epoch_created, + } + } + + /// Create a note with a specific salt (for testing or reconstruction). + pub fn with_salt( + token: Address, + amount: U256, + owner_pubkey: OwnerPubkey, + salt: B256, + epoch_created: Epoch, + ) -> Self { + Self { + token, + amount, + owner_pubkey, + salt, + epoch_created, + } + } + + /// Create a zero-value note (used for padding 2-in-2-out transfers). + pub fn zero(token: Address, owner_pubkey: OwnerPubkey, epoch_created: Epoch) -> Self { + Self::new(token, U256::ZERO, owner_pubkey, epoch_created) + } + + /// Compute the commitment for this note. + /// commitment = poseidon5(token, amount, owner_pubkey, salt, epoch_created) + pub fn commitment(&self) -> Commitment { + // Convert address to B256 (pad with zeros on the left) + let token_b256 = B256::left_padding_from(self.token.as_slice()); + + // Convert amount to B256 + let amount_b256: B256 = self.amount.into(); + + let hash = poseidon5( + token_b256, + amount_b256, + self.owner_pubkey.0, + self.salt, + self.epoch_created.as_field(), + ); + Commitment(hash) + } + + /// Compute the per-epoch nullifier for this note given the spending key. + /// η_e = poseidon3(commitment, spending_key, epoch) + pub fn nullifier(&self, spending_key: &SpendingKey, epoch: Epoch) -> Nullifier { + self.commitment().nullifier(spending_key, epoch) + } + + /// Check if this is a zero-value note. + pub fn is_zero(&self) -> bool { + self.amount == U256::ZERO + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_note_commitment_deterministic() { + let sk = SpendingKey::random(); + let pk = sk.derive_owner_pubkey(); + let salt = B256::ZERO; + let token = Address::ZERO; + let amount = U256::from(1000u64); + + let note1 = Note::with_salt(token, amount, pk, salt, Epoch(2)); + let note2 = Note::with_salt(token, amount, pk, salt, Epoch(2)); + + assert_eq!(note1.commitment(), note2.commitment()); + } + + #[test] + fn test_note_commitment_different_salts() { + let sk = SpendingKey::random(); + let pk = sk.derive_owner_pubkey(); + let token = Address::ZERO; + let amount = U256::from(1000u64); + + let note1 = Note::new(token, amount, pk, Epoch::GENESIS); + let note2 = Note::new(token, amount, pk, Epoch::GENESIS); + + // Different random salts should produce different commitments + assert_ne!(note1.commitment(), note2.commitment()); + } + + #[test] + fn test_note_commitment_binds_epoch() { + // Identical note fields, different epoch_created => different commitment. + let sk = SpendingKey::random(); + let pk = sk.derive_owner_pubkey(); + let salt = B256::ZERO; + let token = Address::ZERO; + let amount = U256::from(1000u64); + + let note0 = Note::with_salt(token, amount, pk, salt, Epoch(0)); + let note1 = Note::with_salt(token, amount, pk, salt, Epoch(1)); + + assert_ne!(note0.commitment(), note1.commitment()); + } + + #[test] + fn test_note_nullifier_per_epoch() { + let sk = SpendingKey::random(); + let pk = sk.derive_owner_pubkey(); + let note = Note::new(Address::ZERO, U256::from(1000u64), pk, Epoch(0)); + + // Deterministic within an epoch... + assert_eq!(note.nullifier(&sk, Epoch(0)), note.nullifier(&sk, Epoch(0))); + // ...and distinct across epochs. + assert_ne!(note.nullifier(&sk, Epoch(0)), note.nullifier(&sk, Epoch(1))); + } + + #[test] + fn test_zero_note() { + let sk = SpendingKey::random(); + let pk = sk.derive_owner_pubkey(); + let note = Note::zero(Address::ZERO, pk, Epoch::GENESIS); + + assert!(note.is_zero()); + assert_eq!(note.amount, U256::ZERO); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain/nullifier.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain/nullifier.rs new file mode 100644 index 0000000..6a67545 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain/nullifier.rs @@ -0,0 +1,42 @@ +use alloy::primitives::B256; +use serde::{ + Deserialize, + Serialize, +}; + +/// A nullifier prevents double-spending by marking a commitment as spent in a +/// given epoch. Only the spending-key holder can compute it. +/// +/// In this extension the nullifier is per-epoch: +/// `η_e = poseidon3(commitment, spending_key, epoch_id)` (see +/// [`crate::domain::commitment::Commitment::nullifier`]). The same note yields a +/// distinct nullifier in each epoch; whether a given `η_e` is "active" (current +/// epoch, becomes the on-chain spend artifact) or "phantom" (a past epoch in +/// which the note was not spent, used only for chain-proof non-membership) is +/// contextual and tracked by the chain-proof / spend layers, not by this value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Nullifier(pub B256); + +impl Nullifier { + /// Create a nullifier from raw bytes. + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(B256::from(bytes)) + } + + /// Get the raw bytes. + pub fn as_bytes(&self) -> &[u8; 32] { + self.0.as_ref() + } +} + +impl From for Nullifier { + fn from(value: B256) -> Self { + Self(value) + } +} + +impl From for B256 { + fn from(value: Nullifier) -> Self { + value.0 + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain/proof.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain/proof.rs new file mode 100644 index 0000000..e3d2324 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain/proof.rs @@ -0,0 +1,248 @@ +//! ZK proof outputs and their public-input layouts. +//! +//! Each `public_inputs_as_array()` returns the public inputs in the exact order +//! the matching circuit declares them in `main` — which is the same order +//! `ShieldedPoolExt` marshals them for on-chain verification. Epochs are +//! field-encoded (`Epoch::as_field`), addresses as left-padded B256, matching +//! the contract. + +use alloy::primitives::{ + Address, + B256, + Bytes, + U256, +}; +use serde::{ + Deserialize, + Serialize, +}; + +use super::epoch::Epoch; + +fn addr_field(addr: Address) -> B256 { + B256::left_padding_from(addr.as_slice()) +} + +// ===== Deposit ===== + +/// Public inputs for the deposit circuit: `[commitment, token, amount, current_epoch]`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DepositPublicInputs { + pub commitment: B256, + pub token: Address, + pub amount: U256, + pub current_epoch: Epoch, +} + +/// A deposit proof and its public inputs. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DepositProof { + pub proof: Bytes, + pub public_inputs: DepositPublicInputs, +} + +impl DepositProof { + pub fn new( + proof: Bytes, + commitment: B256, + token: Address, + amount: U256, + current_epoch: Epoch, + ) -> Self { + Self { + proof, + public_inputs: DepositPublicInputs { commitment, token, amount, current_epoch }, + } + } + + /// `[commitment, token, amount, current_epoch]` (matches `ShieldedPoolExt.deposit`). + pub fn public_inputs_as_array(&self) -> [B256; 4] { + [ + self.public_inputs.commitment, + addr_field(self.public_inputs.token), + self.public_inputs.amount.into(), + self.public_inputs.current_epoch.as_field(), + ] + } +} + +// ===== Transfer (2-in-2-out spend) ===== + +/// Public inputs for the transfer spend circuit (11 fields, circuit order). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransferPublicInputs { + pub nullifiers: [B256; 2], + pub output_commitments: [B256; 2], + pub commitment_root: B256, + pub current_epoch: Epoch, + pub chain_vk_hash: B256, + pub epoch_created_in: [Epoch; 2], + pub chain_accumulator_in: [B256; 2], +} + +/// A transfer spend proof and its public inputs. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransferProof { + pub proof: Bytes, + pub public_inputs: TransferPublicInputs, +} + +impl TransferProof { + /// `[η0, η1, out0, out1, root, current_epoch, chain_vk_hash, ec0, ec1, acc0, acc1]` + /// — matches `circuits/transfer` and `ShieldedPoolExt.transfer`. + pub fn public_inputs_as_array(&self) -> [B256; 11] { + let p = &self.public_inputs; + [ + p.nullifiers[0], + p.nullifiers[1], + p.output_commitments[0], + p.output_commitments[1], + p.commitment_root, + p.current_epoch.as_field(), + p.chain_vk_hash, + p.epoch_created_in[0].as_field(), + p.epoch_created_in[1].as_field(), + p.chain_accumulator_in[0], + p.chain_accumulator_in[1], + ] + } + + pub fn nullifiers(&self) -> [B256; 2] { + self.public_inputs.nullifiers + } + + pub fn output_commitments(&self) -> [B256; 2] { + self.public_inputs.output_commitments + } +} + +// ===== Withdraw (single-input spend) ===== + +/// Public inputs for the withdraw spend circuit (9 fields, circuit order). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WithdrawPublicInputs { + pub nullifier: B256, + pub token: Address, + pub amount: U256, + pub recipient: Address, + pub commitment_root: B256, + pub current_epoch: Epoch, + pub chain_vk_hash: B256, + pub epoch_created_in: Epoch, + pub chain_accumulator_in: B256, +} + +/// A withdraw spend proof and its public inputs. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WithdrawProof { + pub proof: Bytes, + pub public_inputs: WithdrawPublicInputs, +} + +impl WithdrawProof { + /// `[η, token, amount, recipient, root, current_epoch, chain_vk_hash, ec, acc]` + /// — matches `circuits/withdraw` and `ShieldedPoolExt.withdraw`. + pub fn public_inputs_as_array(&self) -> [B256; 9] { + let p = &self.public_inputs; + [ + p.nullifier, + addr_field(p.token), + p.amount.into(), + addr_field(p.recipient), + p.commitment_root, + p.current_epoch.as_field(), + p.chain_vk_hash, + p.epoch_created_in.as_field(), + p.chain_accumulator_in, + ] + } +} + +// ===== Insertion (relayer; k = number of spent nullifiers) ===== + +/// Public inputs for the insertion circuit: `[pre_active_root, post_active_root, +/// pre_leaf_count, η_1..k]`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InsertionPublicInputs { + pub pre_active_root: B256, + pub post_active_root: B256, + pub pre_leaf_count: u64, + pub nullifiers: Vec, +} + +/// An insertion proof and its public inputs. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InsertionProof { + pub proof: Bytes, + pub public_inputs: InsertionPublicInputs, +} + +impl InsertionProof { + /// `[pre_active_root, post_active_root, pre_leaf_count, η_1..k]` — matches + /// `circuits/insertion` and the insertion inputs in `ShieldedPoolExt`. + pub fn public_inputs_as_array(&self) -> Vec { + let p = &self.public_inputs; + let mut inputs = Vec::with_capacity(3 + p.nullifiers.len()); + inputs.push(p.pre_active_root); + inputs.push(p.post_active_root); + inputs.push(B256::from(U256::from(p.pre_leaf_count))); + inputs.extend_from_slice(&p.nullifiers); + inputs + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn b(n: u64) -> B256 { + B256::from(U256::from(n)) + } + + #[test] + fn deposit_inputs_in_circuit_order() { + let p = DepositProof::new(Bytes::new(), b(1), Address::ZERO, U256::from(50u64), Epoch(3)); + let arr = p.public_inputs_as_array(); + assert_eq!(arr[0], b(1)); // commitment + assert_eq!(arr[2], b(50)); // amount + assert_eq!(arr[3], Epoch(3).as_field()); // current_epoch + } + + #[test] + fn transfer_inputs_in_circuit_order() { + let p = TransferProof { + proof: Bytes::new(), + public_inputs: TransferPublicInputs { + nullifiers: [b(10), b(11)], + output_commitments: [b(20), b(21)], + commitment_root: b(30), + current_epoch: Epoch(5), + chain_vk_hash: b(99), + epoch_created_in: [Epoch(2), Epoch(3)], + chain_accumulator_in: [b(40), b(41)], + }, + }; + let arr = p.public_inputs_as_array(); + assert_eq!(arr.len(), 11); + assert_eq!(arr[0], b(10)); // η0 + assert_eq!(arr[5], Epoch(5).as_field()); // current_epoch + assert_eq!(arr[6], b(99)); // chain_vk_hash + assert_eq!(arr[7], Epoch(2).as_field()); // epoch_created_in_0 + assert_eq!(arr[10], b(41)); // chain_accumulator_in_1 + } + + #[test] + fn insertion_inputs_prepend_roots_and_count() { + let p = InsertionProof { + proof: Bytes::new(), + public_inputs: InsertionPublicInputs { + pre_active_root: b(1), + post_active_root: b(2), + pre_leaf_count: 7, + nullifiers: vec![b(10), b(11)], + }, + }; + let arr = p.public_inputs_as_array(); + assert_eq!(arr, vec![b(1), b(2), b(7), b(10), b(11)]); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/domain/witness.rs b/pocs/private-payment/shielded-pool-extension/src/lib/domain/witness.rs new file mode 100644 index 0000000..461c312 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/domain/witness.rs @@ -0,0 +1,259 @@ +//! Witness (prover-input) types for the extended deposit / spend circuits. +//! +//! These carry only domain data — notes, keys, commitment-membership proofs, and +//! per-input chain-proof *state*. The recursive chain-proof *artifacts* (bb proof +//! bytes / field-encoded public inputs) and the insertion witness are prover- and +//! relayer-side concerns wired where their adapters live, not here. No attestation +//! data: the extension's deposit circuit does not enforce KYC attestation (see +//! README "Implementation shortcuts"). + +use alloy::primitives::{ + Address, + B256, +}; +use serde::{ + Deserialize, + Serialize, +}; + +use super::{ + chain_proof::ChainProof, + epoch::Epoch, + keys::SpendingKey, + merkle::CommitmentMerkleProof, + note::Note, +}; + +/// Inputs for the deposit circuit. The note's `epoch_created` is the epoch the +/// deposit mints into; the contract pins it as the proof's `current_epoch`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DepositWitness { + pub note: Note, +} + +impl DepositWitness { + pub fn new(note: Note) -> Self { + Self { note } + } + + /// The note commitment (the deposit's public commitment). + pub fn commitment(&self) -> B256 { + self.note.commitment().0 + } + + /// The epoch the deposit mints into (`= note.epoch_created`). + pub fn current_epoch(&self) -> Epoch { + self.note.epoch_created + } +} + +/// Inputs for the transfer spend circuit (2-in-2-out). Carries the per-input +/// chain-proof state; the spend is valid only when each chain proof is caught up +/// to `current_epoch` (`validate_chain_proofs`). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransferWitness { + pub spending_key: SpendingKey, + pub input_notes: [Note; 2], + pub output_notes: [Note; 2], + pub input_proofs: [CommitmentMerkleProof; 2], + pub chain_proofs: [ChainProof; 2], + pub commitment_root: B256, + pub current_epoch: Epoch, +} + +impl TransferWitness { + pub fn new( + spending_key: SpendingKey, + input_notes: [Note; 2], + output_notes: [Note; 2], + input_proofs: [CommitmentMerkleProof; 2], + chain_proofs: [ChainProof; 2], + commitment_root: B256, + current_epoch: Epoch, + ) -> Self { + Self { + spending_key, + input_notes, + output_notes, + input_proofs, + chain_proofs, + commitment_root, + current_epoch, + } + } + + /// Per-input active nullifiers `η = poseidon3(commitment, sk, current_epoch)`. + pub fn nullifiers(&self) -> [B256; 2] { + [ + self.input_notes[0].nullifier(&self.spending_key, self.current_epoch).0, + self.input_notes[1].nullifier(&self.spending_key, self.current_epoch).0, + ] + } + + /// Output-note commitments (minted at `current_epoch`). + pub fn output_commitments(&self) -> [B256; 2] { + [self.output_notes[0].commitment().0, self.output_notes[1].commitment().0] + } + + /// Input value equals output value (zero/padding inputs contribute 0). + pub fn validate_amounts(&self) -> bool { + let input_sum = self.input_notes[0].amount + self.input_notes[1].amount; + let output_sum = self.output_notes[0].amount + self.output_notes[1].amount; + input_sum == output_sum + } + + /// All notes share one token. + pub fn validate_token_consistency(&self) -> bool { + let token = self.input_notes[0].token; + self.input_notes[1].token == token + && self.output_notes[0].token == token + && self.output_notes[1].token == token + } + + /// Each non-padding input's chain proof is about that note and caught up to + /// `current_epoch` (a zero/padding input carries no real chain proof — the + /// circuit skips its recursive verify). + pub fn validate_chain_proofs(&self) -> bool { + (0..2).all(|i| { + self.input_notes[i].is_zero() || { + let cp = &self.chain_proofs[i]; + cp.commitment == self.input_notes[i].commitment() + && cp.epoch_created == self.input_notes[i].epoch_created + && cp.epoch_validated_through == self.current_epoch + } + }) + } +} + +/// Inputs for the withdraw spend circuit (single input, full-value exit). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WithdrawWitness { + pub spending_key: SpendingKey, + pub note: Note, + pub commitment_proof: CommitmentMerkleProof, + pub chain_proof: ChainProof, + pub commitment_root: B256, + pub current_epoch: Epoch, + pub recipient: Address, +} + +impl WithdrawWitness { + pub fn new( + spending_key: SpendingKey, + note: Note, + commitment_proof: CommitmentMerkleProof, + chain_proof: ChainProof, + commitment_root: B256, + current_epoch: Epoch, + recipient: Address, + ) -> Self { + Self { + spending_key, + note, + commitment_proof, + chain_proof, + commitment_root, + current_epoch, + recipient, + } + } + + /// The active nullifier `η = poseidon3(commitment, sk, current_epoch)`. + pub fn nullifier(&self) -> B256 { + self.note.nullifier(&self.spending_key, self.current_epoch).0 + } + + /// The chain proof is about this note and caught up to `current_epoch`. + pub fn validate_chain_proof(&self) -> bool { + self.chain_proof.commitment == self.note.commitment() + && self.chain_proof.epoch_created == self.note.epoch_created + && self.chain_proof.epoch_validated_through == self.current_epoch + } +} + +#[cfg(test)] +mod tests { + use alloy::primitives::U256; + + use super::*; + + fn caught_up_chain(note: &Note, current_epoch: Epoch) -> ChainProof { + // A chain proof about `note`, reported as validated through `current_epoch`. + let mut cp = ChainProof::genesis(note.commitment(), note.epoch_created); + cp.epoch_validated_through = current_epoch; + cp + } + + #[test] + fn transfer_validations_pass_for_consistent_witness() { + let sk = SpendingKey::random(); + let pk = sk.derive_owner_pubkey(); + let token = Address::ZERO; + let inputs = [ + Note::new(token, U256::from(600u64), pk, Epoch(0)), + Note::new(token, U256::from(400u64), pk, Epoch(1)), + ]; + let outputs = [ + Note::new(token, U256::from(700u64), pk, Epoch(2)), + Note::new(token, U256::from(300u64), pk, Epoch(2)), + ]; + let proof = CommitmentMerkleProof::new(vec![], vec![], 0); + let chains = [caught_up_chain(&inputs[0], Epoch(2)), caught_up_chain(&inputs[1], Epoch(2))]; + let w = TransferWitness::new( + sk, + inputs, + outputs, + [proof.clone(), proof], + chains, + B256::ZERO, + Epoch(2), + ); + + assert!(w.validate_amounts()); + assert!(w.validate_token_consistency()); + assert!(w.validate_chain_proofs()); + assert_eq!(w.output_commitments()[0], w.output_notes[0].commitment().0); + } + + #[test] + fn transfer_rejects_stale_chain_proof() { + let sk = SpendingKey::random(); + let pk = sk.derive_owner_pubkey(); + let token = Address::ZERO; + let inputs = [ + Note::new(token, U256::from(1000u64), pk, Epoch(0)), + Note::zero(token, pk, Epoch(2)), + ]; + let outputs = [ + Note::new(token, U256::from(1000u64), pk, Epoch(2)), + Note::zero(token, pk, Epoch(2)), + ]; + let proof = CommitmentMerkleProof::new(vec![], vec![], 0); + // Input 0's chain proof only validated through epoch 1, but spending in 2. + let stale = caught_up_chain(&inputs[0], Epoch(1)); + let chains = [stale, ChainProof::genesis(inputs[1].commitment(), Epoch(2))]; + let w = TransferWitness::new(sk, inputs, outputs, [proof.clone(), proof], chains, B256::ZERO, Epoch(2)); + + assert!(!w.validate_chain_proofs(), "stale chain proof must be rejected"); + } + + #[test] + fn withdraw_nullifier_and_chain_validation() { + let sk = SpendingKey::random(); + let pk = sk.derive_owner_pubkey(); + let note = Note::new(Address::ZERO, U256::from(500u64), pk, Epoch(1)); + let chain = caught_up_chain(¬e, Epoch(3)); + let w = WithdrawWitness::new( + sk.clone(), + note.clone(), + CommitmentMerkleProof::new(vec![], vec![], 0), + chain, + B256::ZERO, + Epoch(3), + Address::repeat_byte(0xBB), + ); + + assert!(w.validate_chain_proof()); + assert_eq!(w.nullifier(), note.nullifier(&sk, Epoch(3)).0); + } +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/lib.rs b/pocs/private-payment/shielded-pool-extension/src/lib/lib.rs new file mode 100644 index 0000000..39f0f34 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/lib.rs @@ -0,0 +1,4 @@ +pub mod adapters; +pub mod crypto; +pub mod domain; +pub mod ports; diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/ports.rs b/pocs/private-payment/shielded-pool-extension/src/lib/ports.rs new file mode 100644 index 0000000..39d6835 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/ports.rs @@ -0,0 +1,3 @@ +pub mod pir; +pub mod root_verifier; +pub mod state_replica; diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/ports/pir.rs b/pocs/private-payment/shielded-pool-extension/src/lib/ports/pir.rs new file mode 100644 index 0000000..295e24e --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/ports/pir.rs @@ -0,0 +1,26 @@ +//! PIR client port: the single PIR'd read, the commitment-tree membership path. + +use crate::domain::merkle::CommitmentMerkleProof; + +/// Errors from a PIR membership-path fetch. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum PirError { + #[error("leaf index {0} is out of range")] + LeafOutOfRange(u64), + #[error("commitment tree is empty")] + EmptyTree, +} + +/// The single PIR'd read: the commitment-tree membership path for a leaf the +/// wallet owns (its index is known from its own minting event). +/// +/// Two adapters implement this trait. The `CleartextPirClient` dev stub sends the +/// leaf index to the server (which learns it) — convenient for tests, not private. +/// The real `SimplePirClient` computes the sibling-node offsets locally and +/// fetches each node obliviously over the `tree-pir` flattened array, so the leaf +/// index never leaves the wallet. Phantom-epoch lookups and the active tree are +/// NOT PIR'd (see `StateReplicaQuery` and SPEC.md "Off-Chain State-Replica Server"). +pub trait PirClient { + /// Fetch the membership path for the commitment at `leaf_index`. + fn fetch_membership_path(&self, leaf_index: u64) -> Result; +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/ports/root_verifier.rs b/pocs/private-payment/shielded-pool-extension/src/lib/ports/root_verifier.rs new file mode 100644 index 0000000..963ab79 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/ports/root_verifier.rs @@ -0,0 +1,31 @@ +//! Root-authenticity port: verify a contract storage slot against a +//! consensus-anchored state root, replacing the trusted-RPC root reads. +//! +//! The wallet reconstructs `commitment_root` / `frozenNullifierRoots[e]` against +//! roots it trusts; this port is what makes those roots trustworthy. The +//! production implementation is a Helios light client (its finalized header's +//! `state_root` is consensus-verified); the PoC ships a `TrustedRootVerifier` that +//! takes a supplied state root (an anvil block's), since Helios needs a beacon +//! chain that anvil lacks. Either way the on-chain verification (the two-level MPT +//! proof) is identical — see `adapters::light_client`. + +use alloy::rpc::types::EIP1186AccountProofResponse; + +/// Errors from storage-proof verification. +#[derive(Debug, thiserror::Error)] +pub enum RootVerifierError { + #[error("account proof invalid: {0}")] + Account(String), + #[error("storage proof invalid: {0}")] + Storage(String), + #[error("proof carries no storage entry for the requested slot")] + MissingSlot, +} + +/// Verifies an `eth_getProof` response against a state root the implementation +/// trusts (consensus-verified in production), returning the proven slot value. +pub trait RootVerifier { + /// Verify `proof` (an `eth_getProof` result for one slot) and return the + /// verified storage value. + fn verify_storage(&self, proof: &EIP1186AccountProofResponse) -> Result; +} diff --git a/pocs/private-payment/shielded-pool-extension/src/lib/ports/state_replica.rs b/pocs/private-payment/shielded-pool-extension/src/lib/ports/state_replica.rs new file mode 100644 index 0000000..d22a752 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/src/lib/ports/state_replica.rs @@ -0,0 +1,43 @@ +//! State-replica port: the events the replica ingests and the cleartext queries +//! the wallet makes against it. +//! +//! The commitment membership path (the PIR'd read) is fetched separately via +//! [`crate::ports::pir::PirClient`]. The replica is untrusted for correctness: +//! served witnesses are re-checked in-circuit against light-client-verified +//! roots. See SPEC.md "Off-Chain State-Replica Server". + +use alloy::primitives::B256; + +use crate::domain::indexed_merkle::NonMembershipWitness; + +/// On-chain events the replica ingests to rebuild public state. Mirrors the +/// `ShieldedPoolExt` events; produced live by the RPC adapter (later slice) or +/// fed synthetically in tests. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Event { + /// A deposit appended `commitment` to the commitment tree. + Deposit { commitment: B256 }, + /// A transfer appended `output_commitments` and spent the public + /// `nullifiers` (the η_active list) into the active nullifier tree. + Transfer { + nullifiers: Vec, + output_commitments: Vec, + }, + /// A withdraw spent `nullifiers` into the active nullifier tree. + Withdraw { nullifiers: Vec }, + /// Epoch `epoch` rolled over: its active tree is frozen and the active tree + /// resets for `epoch + 1`. + EpochRollover { epoch: u64 }, +} + +/// Cleartext queries the wallet makes to the state-replica server. +pub trait StateReplicaQuery { + /// Sorted-low-leaf non-membership witness for `nullifier` against the frozen + /// tree of `epoch`. `None` if the epoch is not frozen or `nullifier` is present. + fn phantom_witness(&self, epoch: u64, nullifier: B256) -> Option; + + /// Root of a frozen epoch's nullifier tree. Untrusted (light-client-verified + /// in a later slice); served so the wallet/circuit knows what root to + /// reconstruct a phantom witness against. + fn frozen_nullifier_root(&self, epoch: u64) -> Option; +} diff --git a/pocs/private-payment/shielded-pool-extension/tests/integration.rs b/pocs/private-payment/shielded-pool-extension/tests/integration.rs new file mode 100644 index 0000000..d6cff40 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/tests/integration.rs @@ -0,0 +1,441 @@ +//! In-process end-to-end test for the shielded-pool extension. +//! +//! Self-contained: spawns `anvil`, deploys the real bb-generated verifiers + +//! `MockERC20` + `ShieldedPoolExt` via a forge script (forge auto-links the +//! contract libraries), and drives the full flow with REAL proofs (bb). It +//! exercises every novel mechanism on-chain: the `epoch_created` deposit binding, +//! epoch rollover, the per-note IVC chain proof (genesis + one frozen-epoch +//! extend, recursively verified by the spend), the two-proof spend (wallet spend +//! proof + relayer insertion proof, bound by the shared nullifier list), and the +//! active-tree advance. +//! +//! Covers both spend flows (deposit -> rollover -> transfer, and the same through +//! withdraw): transfer exercises the k=2 insertion proof, withdraw the k=1 one. +//! +//! Heavy (real recursive proving + anvil + forge): `#[ignore]`d. Run with: +//! cargo test --test integration -- --ignored --nocapture +//! +//! Prereqs: `anvil`, `forge`, `nargo` 1.0.0-beta.21, `bb` 5.0.0-nightly on PATH. + +use std::{ + path::{ + Path, + PathBuf, + }, + process::Command, +}; + +use alloy::{ + node_bindings::Anvil, + primitives::{ + Address, + B256, + Bytes, + U256, + }, +}; +use private_payment_shielded_pool_extension::{ + adapters::{ + bb_prover::{ + field_to_decimal, + BbProver, + ChainUpdateArtifact, + ChainUpdateInput, + DepositInput, + InsertionToml, + TransferInput, + WithdrawInput, + CHAIN_PROOF_PUB_LEN, + RECURSIVE_PROOF_LENGTH, + ULTRA_VK_LENGTH_IN_FIELDS, + }, + ethereum_rpc::EthereumRpc, + indexed_merkle_tree::IndexedMerkleTree, + }, + domain::{ + chain_proof::expected_accumulator, + commitment::Commitment, + epoch::Epoch, + indexed_merkle::NonMembershipWitness, + keys::SpendingKey, + note::Note, + }, +}; + +const DEV_KEY: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + +fn project_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) +} + +fn fd(b: B256) -> String { + field_to_decimal(b) +} + +fn token_field(token: Address) -> B256 { + B256::left_padding_from(token.as_slice()) +} + +fn zeros(n: usize) -> Vec { + vec!["0".to_string(); n] +} + +fn index_bits(index: u64) -> Vec { + (0..32).map(|i| (index >> i) & 1 == 1).collect() +} + +fn decimal_to_b256(decimal: &str) -> B256 { + B256::from(U256::from_str_radix(decimal, 10).expect("vk_hash decimal")) +} + +/// Deploy everything via the forge script (forge deploys + links the libraries), +/// returning `(pool, token)` read back from `deployments.toml`. +fn deploy(root: &Path, endpoint: &str, chain_vk_hash: B256, empty_imt_root: B256) -> (Address, Address) { + // Seed the [31337] chain skeleton the forge-std `Config` base reads on load + // (it requires the file to exist with the chain table); the deploy script + // then records the deployed addresses under [31337.address]. + std::fs::write(root.join("deployments.toml"), format!("[31337]\nendpoint_url = \"{endpoint}\"\n")) + .expect("write deployments.toml skeleton"); + + let status = Command::new("forge") + .args([ + "script", + "contracts/script/Deploy.s.sol:Deploy", + "--rpc-url", + endpoint, + "--broadcast", + "--private-key", + DEV_KEY, + ]) + .current_dir(root) + .env("CHAIN_VK_HASH", format!("0x{}", hex::encode(chain_vk_hash))) + .env("EMPTY_IMT_ROOT", format!("0x{}", hex::encode(empty_imt_root))) + .status() + .expect("run forge script"); + assert!(status.success(), "forge deploy failed"); + + let content = std::fs::read_to_string(root.join("deployments.toml")).expect("deployments.toml"); + let v: toml::Value = content.parse().expect("parse deployments.toml"); + // `Config` writes chain-keyed tables: [31337.address] = . + let addrs = &v["31337"]["address"]; + let pool = addrs["shielded_pool"].as_str().unwrap().parse().expect("pool address"); + let token = addrs["mock_token"].as_str().unwrap().parse().expect("token address"); + (pool, token) +} + +/// Genesis (base-case) chain-update input for a note. +fn chain_genesis(vk_hash: &str, commitment: Commitment, sk: &SpendingKey, token: Address, amount: U256, salt: B256) -> ChainUpdateInput { + ChainUpdateInput { + commitment: fd(commitment.0), + epoch_created: "0".into(), + epoch_validated_through: "0".into(), + accumulator: "0".into(), + fixed_vk_hash: vk_hash.into(), + is_base_case: true, + prior_vk: zeros(ULTRA_VK_LENGTH_IN_FIELDS), + prior_proof: zeros(RECURSIVE_PROOF_LENGTH), + prior_public_inputs: zeros(CHAIN_PROOF_PUB_LEN), + frozen_root_next: "0".into(), + spending_key: fd(sk.0), + token: fd(token_field(token)), + amount: fd(B256::from(amount)), + salt: fd(salt), + low_value: "0".into(), + low_next_value: "0".into(), + low_next_index: "0".into(), + path_bits: vec![false; 32], + siblings: zeros(32), + } +} + +/// One chain-update step: fold `frozen_root` (epoch the note wasn't spent in), +/// recursively verifying `prior`. +#[allow(clippy::too_many_arguments)] +fn chain_step( + vk_hash: &str, + vk_fields: &[String], + commitment: Commitment, + sk: &SpendingKey, + token: Address, + amount: U256, + salt: B256, + prior: &ChainUpdateArtifact, + frozen_root: B256, + accumulator: B256, + nmw: &NonMembershipWitness, +) -> ChainUpdateInput { + ChainUpdateInput { + commitment: fd(commitment.0), + epoch_created: "0".into(), + epoch_validated_through: "1".into(), + accumulator: fd(accumulator), + fixed_vk_hash: vk_hash.into(), + is_base_case: false, + prior_vk: vk_fields.to_vec(), + prior_proof: prior.proof.clone(), + prior_public_inputs: prior.public_inputs.clone(), + frozen_root_next: fd(frozen_root), + spending_key: fd(sk.0), + token: fd(token_field(token)), + amount: fd(B256::from(amount)), + salt: fd(salt), + low_value: fd(nmw.low_leaf.value), + low_next_value: fd(nmw.low_leaf.next_value), + low_next_index: nmw.low_leaf.next_index.to_string(), + path_bits: index_bits(nmw.low_leaf_index), + siblings: nmw.siblings.iter().map(|s| fd(*s)).collect(), + } +} + +#[tokio::test] +#[ignore = "spawns anvil + forge + proves real circuits (minutes); run with --ignored"] +async fn transfer_flow_verifies_on_chain() { + let root = project_root(); + let prover = BbProver::new(root.clone()); + + // Off-chain constructor params: chainVkHash from bb, emptyImtRoot from the IMT. + let (vk_fields, vk_hash) = prover.write_chain_update_vk().expect("chain vk"); + let chain_vk_hash = decimal_to_b256(&vk_hash); + let empty_imt_root = IndexedMerkleTree::new().root(); + + let anvil = Anvil::new().spawn(); + let endpoint = anvil.endpoint(); + let (pool, token) = deploy(&root, &endpoint, chain_vk_hash, empty_imt_root); + + let rpc = EthereumRpc::new(&endpoint, DEV_KEY, pool).await.expect("rpc"); + let deployer = rpc.signer_address(); + rpc.add_supported_token(token).await.expect("add token"); + let amount = U256::from(1000u64); + rpc.mint_mock_token(token, deployer, amount).await.expect("mint"); + rpc.approve_token(token, amount).await.expect("approve"); + + let sk = SpendingKey::from_bytes([3u8; 32]); + let owner = sk.derive_owner_pubkey(); + let salt_a = B256::repeat_byte(0x07); + let note_a = Note::with_salt(token, amount, owner, salt_a, Epoch(0)); + let commit_a = note_a.commitment(); + + // ===== 1. Alice deposits at epoch 0 (real deposit proof) ===== + prover.write_evm_vk("deposit").expect("deposit vk"); + let dep = DepositInput { + commitment: fd(commit_a.0), + token: fd(token_field(token)), + amount: fd(B256::from(amount)), + current_epoch_at_deposit: 0, + owner_pubkey: fd(owner.0), + salt: fd(salt_a), + }; + let dep_proof = prover.prove_evm("deposit", &dep).expect("prove deposit"); + rpc.deposit(Bytes::from(dep_proof.proof), commit_a.0, token, amount, Bytes::new()) + .await + .expect("on-chain deposit"); + assert_eq!(rpc.commitment_count().await.unwrap(), 1); + let commitment_root = rpc.commitment_root().await.unwrap(); // single-leaf tree => commit_a + + // ===== 2. Rollover to epoch 1 (freezes the empty epoch-0 active tree) ===== + rpc.rollover_epoch().await.expect("rollover"); + assert_eq!(rpc.current_epoch().await.unwrap(), 1); + let frozen0 = rpc.frozen_nullifier_root(0).await.unwrap(); + + // ===== 3. Alice's chain proof: genesis -> extend over frozen epoch 0 ===== + let genesis = chain_genesis(&vk_hash, commit_a, &sk, token, amount, salt_a); + let genesis_art = prover.prove_chain_update(&genesis).expect("genesis chain proof"); + + let frozen_tree = IndexedMerkleTree::new(); // empty epoch-0 tree (root == frozen0) + assert_eq!(frozen_tree.root(), frozen0, "Rust empty IMT root == on-chain frozenNullifierRoots[0]"); + let phantom_eta_0 = commit_a.nullifier(&sk, Epoch(0)).0; + let nmw = frozen_tree.non_membership_witness(phantom_eta_0).expect("non-membership"); + let acc1 = expected_accumulator(&[frozen0]); + let step = chain_step(&vk_hash, &vk_fields, commit_a, &sk, token, amount, salt_a, &genesis_art, frozen0, acc1, &nmw); + let chain_art = prover.prove_chain_update(&step).expect("chain step (caught up to epoch 1)"); + + // ===== 4. Transfer: Alice's note -> 600 + 400 to herself; input 1 padding ===== + let pad = Note::with_salt(token, U256::ZERO, owner, B256::repeat_byte(0x09), Epoch(1)); + let out0 = Note::with_salt(token, U256::from(600u64), owner, B256::repeat_byte(0x01), Epoch(1)); + let out1 = Note::with_salt(token, U256::from(400u64), owner, B256::repeat_byte(0x02), Epoch(1)); + let eta0 = commit_a.nullifier(&sk, Epoch(1)).0; + let eta1 = pad.commitment().nullifier(&sk, Epoch(1)).0; + + let ti = TransferInput { + nullifier_active_0: fd(eta0), + nullifier_active_1: fd(eta1), + commitment_out_0: fd(out0.commitment().0), + commitment_out_1: fd(out1.commitment().0), + commitment_root: fd(commitment_root), + current_epoch: "1".into(), + chain_vk_hash: vk_hash.clone(), + epoch_created_in_0: "0".into(), + epoch_created_in_1: "1".into(), + chain_accumulator_in_0: fd(acc1), + chain_accumulator_in_1: "0".into(), + spending_key: fd(sk.0), + token_in_0: fd(token_field(token)), + amount_in_0: 1000, + salt_in_0: fd(salt_a), + token_in_1: fd(token_field(token)), + amount_in_1: 0, + salt_in_1: fd(B256::repeat_byte(0x09)), + token_out_0: fd(token_field(token)), + amount_out_0: 600, + owner_out_0: fd(owner.0), + salt_out_0: fd(B256::repeat_byte(0x01)), + token_out_1: fd(token_field(token)), + amount_out_1: 400, + owner_out_1: fd(owner.0), + salt_out_1: fd(B256::repeat_byte(0x02)), + proof_length: 0, + path_0: zeros(32), + indices_0: vec![false; 32], + path_1: zeros(32), + indices_1: vec![false; 32], + chain_vk: vk_fields.clone(), + chain_proof_0: chain_art.proof.clone(), + chain_pub_0: chain_art.public_inputs.clone(), + chain_proof_1: zeros(RECURSIVE_PROOF_LENGTH), + chain_pub_1: zeros(CHAIN_PROOF_PUB_LEN), + }; + prover.write_evm_vk("transfer").expect("transfer vk"); + let spend = prover.prove_evm("transfer", &ti).expect("prove transfer"); + + // ===== 5. Relayer insertion proof for [eta0, eta1] into the active tree ===== + let mut active = IndexedMerkleTree::new(); // epoch-1 active tree starts empty + let pre_root = active.root(); + let pre_count = active.leaf_count(); + let w0 = active.insert(eta0).expect("insert eta0"); + let w1 = active.insert(eta1).expect("insert eta1"); + let post_root = active.root(); + let it = InsertionToml::from_witnesses(pre_root, post_root, pre_count, &[eta0, eta1], &[w0, w1]); + prover.write_evm_vk("insertion").expect("insertion vk"); + let ins = prover.prove_evm("insertion", &it).expect("prove insertion"); + + // ===== 6. Submit both proofs on-chain ===== + rpc.transfer( + Bytes::from(spend.proof), + Bytes::from(ins.proof), + [eta0, eta1], + [out0.commitment().0, out1.commitment().0], + commitment_root, + [0, 1], + post_root, + Bytes::new(), + ) + .await + .expect("on-chain transfer (spend + insertion proofs)"); + + assert_eq!(rpc.active_nullifier_root().await.unwrap(), post_root, "active root advanced"); + assert_eq!(rpc.active_leaf_count().await.unwrap(), 3, "two nullifiers appended (1 -> 3)"); + assert_eq!(rpc.commitment_count().await.unwrap(), 3, "two outputs appended (1 -> 3)"); +} + +#[tokio::test] +#[ignore = "spawns anvil + forge + proves real circuits (minutes); run with --ignored"] +async fn withdraw_flow_verifies_on_chain() { + let root = project_root(); + let prover = BbProver::new(root.clone()); + + // Off-chain constructor params: chainVkHash from bb, emptyImtRoot from the IMT. + let (vk_fields, vk_hash) = prover.write_chain_update_vk().expect("chain vk"); + let chain_vk_hash = decimal_to_b256(&vk_hash); + let empty_imt_root = IndexedMerkleTree::new().root(); + + let anvil = Anvil::new().spawn(); + let endpoint = anvil.endpoint(); + let (pool, token) = deploy(&root, &endpoint, chain_vk_hash, empty_imt_root); + + let rpc = EthereumRpc::new(&endpoint, DEV_KEY, pool).await.expect("rpc"); + let deployer = rpc.signer_address(); + rpc.add_supported_token(token).await.expect("add token"); + let amount = U256::from(1000u64); + rpc.mint_mock_token(token, deployer, amount).await.expect("mint"); + rpc.approve_token(token, amount).await.expect("approve"); + + let sk = SpendingKey::from_bytes([3u8; 32]); + let owner = sk.derive_owner_pubkey(); + let salt_a = B256::repeat_byte(0x07); + let note_a = Note::with_salt(token, amount, owner, salt_a, Epoch(0)); + let commit_a = note_a.commitment(); + + // ===== 1. Deposit at epoch 0 (real deposit proof) ===== + prover.write_evm_vk("deposit").expect("deposit vk"); + let dep = DepositInput { + commitment: fd(commit_a.0), + token: fd(token_field(token)), + amount: fd(B256::from(amount)), + current_epoch_at_deposit: 0, + owner_pubkey: fd(owner.0), + salt: fd(salt_a), + }; + let dep_proof = prover.prove_evm("deposit", &dep).expect("prove deposit"); + rpc.deposit(Bytes::from(dep_proof.proof), commit_a.0, token, amount, Bytes::new()) + .await + .expect("on-chain deposit"); + let commitment_root = rpc.commitment_root().await.unwrap(); // single-leaf tree => commit_a + + // ===== 2. Rollover to epoch 1 (freezes the empty epoch-0 active tree) ===== + rpc.rollover_epoch().await.expect("rollover"); + let frozen0 = rpc.frozen_nullifier_root(0).await.unwrap(); + + // ===== 3. Chain proof: genesis -> extend over frozen epoch 0 ===== + let genesis = chain_genesis(&vk_hash, commit_a, &sk, token, amount, salt_a); + let genesis_art = prover.prove_chain_update(&genesis).expect("genesis chain proof"); + let frozen_tree = IndexedMerkleTree::new(); // empty epoch-0 tree (root == frozen0) + let phantom_eta_0 = commit_a.nullifier(&sk, Epoch(0)).0; + let nmw = frozen_tree.non_membership_witness(phantom_eta_0).expect("non-membership"); + let acc1 = expected_accumulator(&[frozen0]); + let step = chain_step(&vk_hash, &vk_fields, commit_a, &sk, token, amount, salt_a, &genesis_art, frozen0, acc1, &nmw); + let chain_art = prover.prove_chain_update(&step).expect("chain step (caught up to epoch 1)"); + + // ===== 4. Withdraw: note_a's full 1000 exits to `recipient` (1 input, no change) ===== + let recipient = Address::repeat_byte(0xBB); + let eta = commit_a.nullifier(&sk, Epoch(1)).0; + let wi = WithdrawInput { + nullifier_active: fd(eta), + token: fd(token_field(token)), + amount: 1000, + recipient: fd(token_field(recipient)), + commitment_root: fd(commitment_root), + current_epoch: "1".into(), + chain_vk_hash: vk_hash.clone(), + epoch_created_in: "0".into(), + chain_accumulator_in: fd(acc1), + spending_key: fd(sk.0), + salt: fd(salt_a), + proof_length: 0, + path: zeros(32), + indices: vec![false; 32], + chain_vk: vk_fields.clone(), + chain_proof: chain_art.proof.clone(), + chain_pub: chain_art.public_inputs.clone(), + }; + prover.write_evm_vk("withdraw").expect("withdraw vk"); + let spend = prover.prove_evm("withdraw", &wi).expect("prove withdraw"); + + // ===== 5. Relayer k=1 insertion proof for [eta] (the insertion_withdraw circuit) ===== + let mut active = IndexedMerkleTree::new(); // epoch-1 active tree starts empty + let pre_root = active.root(); + let pre_count = active.leaf_count(); + let w = active.insert(eta).expect("insert eta"); + let post_root = active.root(); + let it = InsertionToml::from_witnesses(pre_root, post_root, pre_count, &[eta], &[w]); + prover.write_evm_vk("insertion_withdraw").expect("insertion_withdraw vk"); + let ins = prover.prove_evm("insertion_withdraw", &it).expect("prove k=1 insertion"); + + // ===== 6. Submit both proofs on-chain (exercises the real k=1 withdraw verifier) ===== + rpc.withdraw( + Bytes::from(spend.proof), + Bytes::from(ins.proof), + eta, + token, + amount, + recipient, + commitment_root, + 0, + post_root, + ) + .await + .expect("on-chain withdraw (spend + k=1 insertion proofs)"); + + assert_eq!(rpc.active_nullifier_root().await.unwrap(), post_root, "active root advanced"); + assert_eq!(rpc.active_leaf_count().await.unwrap(), 2, "one nullifier appended (1 -> 2)"); + assert_eq!(rpc.commitment_count().await.unwrap(), 1, "withdraw adds no output commitment"); +} diff --git a/pocs/private-payment/shielded-pool-extension/tests/light_client.rs b/pocs/private-payment/shielded-pool-extension/tests/light_client.rs new file mode 100644 index 0000000..4f77393 --- /dev/null +++ b/pocs/private-payment/shielded-pool-extension/tests/light_client.rs @@ -0,0 +1,82 @@ +//! Light-client root-verification integration test. +//! +//! Spawns anvil, deploys `MockERC20` + mints, then verifies several real storage +//! slots (mixed inclusion + exclusion) through the two-level MPT verifier against +//! the latest block's `stateRoot` — the value Helios supplies (consensus-verified) +//! in production. Demonstrates the on-chain verification half of the light-client +//! check; the consensus verification of the `stateRoot` itself is Helios's job and +//! can't run against anvil (no beacon chain). +//! +//! `#[ignore]` (spawns anvil). Run with: +//! cargo test --test light_client -- --ignored --nocapture + +use alloy::{ + eips::BlockId, + network::EthereumWallet, + primitives::{ + Address, + B256, + U256, + }, + providers::{ + Provider, + ProviderBuilder, + }, + signers::local::PrivateKeySigner, + sol, +}; +use private_payment_shielded_pool_extension::{ + adapters::light_client::TrustedRootVerifier, + ports::root_verifier::RootVerifier, +}; + +sol!( + #[sol(rpc)] + MockERC20, + "contracts/out/MockERC20.sol/MockERC20.json" +); + +const DEV_KEY: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + +#[tokio::test] +#[ignore = "spawns anvil; run with --ignored"] +async fn verifies_real_storage_slots_against_block_state_root() { + let anvil = alloy::node_bindings::Anvil::new().spawn(); + let signer: PrivateKeySigner = DEV_KEY.parse().unwrap(); + let provider = + ProviderBuilder::new().wallet(EthereumWallet::from(signer)).connect_http(anvil.endpoint().parse().unwrap()); + + // Deploy + mint so storage holds both non-zero (inclusion) and zero + // (exclusion, unset mapping base) slots. + let token = *MockERC20::deploy(&provider, "USD Coin".into(), "USDC".into(), 6u8).await.unwrap().address(); + MockERC20::new(token, &provider) + .mint(Address::repeat_byte(0xAB), U256::from(1_000_000u64)) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // Trust the latest block's stateRoot (in production Helios supplies a + // consensus-verified one); verification is identical either way. + let block = provider.get_block(BlockId::latest()).await.unwrap().unwrap(); + let verifier = TrustedRootVerifier::new(block.header.state_root); + + let (mut saw_inclusion, mut saw_exclusion) = (false, false); + for slot_num in 0u64..5 { + let slot = B256::from(U256::from(slot_num)); + let stored = provider.get_storage_at(token, U256::from(slot_num)).await.unwrap(); + let proof = provider.get_proof(token, vec![slot]).await.unwrap(); + + let verified = verifier.verify_storage(&proof).expect("MPT proof must verify"); + assert_eq!(verified, B256::from(stored), "verified value must equal stored slot {slot_num}"); + + if stored.is_zero() { + saw_exclusion = true; + } else { + saw_inclusion = true; + } + } + assert!(saw_inclusion && saw_exclusion, "should exercise both inclusion and exclusion proofs"); +}