diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e8fcd439..97bee4c3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -70,6 +70,25 @@ jobs: - name: ZKEVM opcode run: cargo nextest run --release --manifest-path crates/zkevm_opcode_defs/Cargo.toml + test-zkevm-abstractions-airbender: + name: zk_evm_abstractions airbender delegations + if: ${{ !contains(github.head_ref, 'release-please--branches') }} + runs-on: matterlabs-ci-runner-high-performance + needs: build + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions-rust-lang/setup-rust-toolchain@1780873c7b576612439a134613cc4cc74ce5538c # v1.15.2 + with: + rustflags: "" + - name: Setup rust + run: | + rustup set profile minimal + rustup toolchain install nightly-2024-11-19 + rustup default nightly-2024-11-19 + cargo install cargo-nextest@0.9.108 --locked + - name: ZKEVM abstractions airbender delegation tests + run: cargo nextest run --release --manifest-path crates/zk_evm_abstractions/Cargo.toml --features airbender-precompile-delegations + formatting: name: cargo fmt runs-on: ubuntu-latest @@ -91,6 +110,7 @@ jobs: [ build, test, + test-zkevm-abstractions-airbender, formatting, ] steps: diff --git a/Cargo.lock b/Cargo.lock index 6a48331e..f5e66123 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -22,6 +34,42 @@ dependencies = [ "memchr", ] +[[package]] +name = "airbender-crypto" +version = "0.1.0" +source = "git+https://github.com/matter-labs/airbender-platform?branch=main#321b2d783f90a5621d78409e068ef78f9d8271f2" +dependencies = [ + "ark-bls12-381", + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", + "blake2 0.10.6", + "blake2s_u32", + "cfg-if", + "common_constants", + "const_for", + "educe", + "itertools 0.14.0", + "k256", + "num-bigint 0.4.6", + "num-traits", + "p256", + "ripemd", + "ruint", + "seq-macro", + "sha2 0.10.9", + "sha3 0.10.8", + "zeroize", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "ansi_term" version = "0.12.1" @@ -87,6 +135,142 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.2", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec 0.7.6", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote 1.0.40", + "syn 2.0.117", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 2.0.117", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.2", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec 0.7.6", + "digest 0.10.7", + "num-bigint 0.4.6", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 2.0.117", +] + +[[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.5", +] + [[package]] name = "arr_macro" version = "0.1.3" @@ -248,6 +432,15 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake2s_u32" +version = "0.1.0" +source = "git+https://github.com/matter-labs/zksync-airbender?branch=dev#769ec2e3937fa591221e8f27dab66273c8ab1ffb" +dependencies = [ + "common_constants", + "unroll", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -289,11 +482,11 @@ dependencies = [ "derivative", "ethereum-types", "firestorm", - "itertools", + "itertools 0.10.5", "lazy_static", "num-modular", "num_cpus", - "rand", + "rand 0.8.5", "rayon", "serde", "sha2 0.10.9", @@ -409,7 +602,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2 1.0.94", "quote 1.0.40", - "syn 2.0.100", + "syn 2.0.117", ] [[package]] @@ -433,6 +626,11 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "common_constants" +version = "0.1.0" +source = "git+https://github.com/matter-labs/zksync-airbender?branch=dev#769ec2e3937fa591221e8f27dab66273c8ab1ffb" + [[package]] name = "console" version = "0.15.11" @@ -451,6 +649,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_for" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c50fcfdf972929aff202c16b80086aa3cfc6a3a820af714096c58c7c1d0582" + [[package]] name = "const_format" version = "0.2.34" @@ -564,7 +768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -681,6 +885,18 @@ dependencies = [ "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 1.0.94", + "quote 1.0.40", + "syn 2.0.117", +] + [[package]] name = "either" version = "1.15.0" @@ -701,7 +917,7 @@ dependencies = [ "group", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -713,6 +929,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[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 1.0.94", + "quote 1.0.40", + "syn 2.0.117", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -720,6 +956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", + "regex", ] [[package]] @@ -803,7 +1040,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -820,7 +1057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand", + "rand 0.8.5", "rustc-hex", "static_assertions", ] @@ -831,6 +1068,12 @@ 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 = "franklin-crypto" version = "0.32.10" @@ -848,13 +1091,13 @@ dependencies = [ "digest 0.9.0", "hex", "indexmap 1.9.3", - "itertools", + "itertools 0.10.5", "lazy_static", "num-bigint 0.4.6", "num-derive", "num-integer", "num-traits", - "rand", + "rand 0.8.5", "rand_chacha", "rand_xorshift", "serde", @@ -972,6 +1215,20 @@ dependencies = [ "wasi", ] +[[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", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + [[package]] name = "group" version = "0.13.0" @@ -979,7 +1236,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -994,6 +1251,10 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "foldhash", +] [[package]] name = "heck" @@ -1046,6 +1307,12 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +[[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" @@ -1087,7 +1354,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", - "syn 2.0.100", + "syn 2.0.117", ] [[package]] @@ -1108,6 +1375,7 @@ checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", + "serde", ] [[package]] @@ -1137,6 +1405,24 @@ 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.15" @@ -1172,6 +1458,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[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.171" @@ -1364,7 +1656,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2 1.0.94", "quote 1.0.40", - "syn 2.0.100", + "syn 2.0.117", ] [[package]] @@ -1422,9 +1714,15 @@ dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2 1.0.94", "quote 1.0.40", - "syn 2.0.100", + "syn 2.0.117", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1465,6 +1763,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2 1.0.94", + "syn 2.0.117", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -1554,6 +1862,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quickcheck" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c589f335db0f6aaa168a7cd27b1fc6920f5e1470c804f814d9cd6e62a0f70b" +dependencies = [ + "env_logger 0.11.8", + "log", + "rand 0.10.0", +] + [[package]] name = "quote" version = "0.6.13" @@ -1572,6 +1891,12 @@ dependencies = [ "proc-macro2 1.0.94", ] +[[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" @@ -1586,7 +1911,17 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "getrandom 0.4.2", + "rand_core 0.10.0", ] [[package]] @@ -1596,7 +1931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1605,16 +1940,22 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1684,7 +2025,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "rand_chacha", "rand_xorshift", "serde", @@ -1703,6 +2044,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "rlp" version = "0.5.2" @@ -1713,6 +2063,21 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "ruint" +version = "1.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" +dependencies = [ + "ruint-macro", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + [[package]] name = "rustc-hex" version = "2.1.0" @@ -1745,6 +2110,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "seq-macro" version = "0.3.6" @@ -1768,7 +2139,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", - "syn 2.0.100", + "syn 2.0.117", ] [[package]] @@ -1878,7 +2249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1906,7 +2277,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5427fd5cbe3021b7e92f438edf7675f97afbe1cf942701980b0c52f31718e5a2" dependencies = [ "derivative", - "rand", + "rand 0.8.5", "rescue_poseidon", "serde", ] @@ -2005,9 +2376,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", @@ -2048,7 +2419,7 @@ checksum = "888d0c3c6db53c0fdab160d2ed5e12ba745383d3e85813f2ea0f2b1475ab553f" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", - "syn 2.0.100", + "syn 2.0.117", ] [[package]] @@ -2077,7 +2448,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", - "syn 2.0.100", + "syn 2.0.117", ] [[package]] @@ -2155,7 +2526,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", - "syn 2.0.100", + "syn 2.0.117", ] [[package]] @@ -2306,6 +2677,58 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[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", +] + +[[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.9.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 2.9.0", + "hashbrown 0.15.2", + "indexmap 2.9.0", + "semver", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2428,6 +2851,94 @@ 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-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "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 0.5.0", + "indexmap 2.9.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 1.0.94", + "quote 1.0.40", + "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 2.9.0", + "indexmap 2.9.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.9.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid 0.2.6", + "wasmparser", +] + [[package]] name = "wyz" version = "0.5.1" @@ -2454,7 +2965,7 @@ checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" dependencies = [ "proc-macro2 1.0.94", "quote 1.0.40", - "syn 2.0.100", + "syn 2.0.117", ] [[package]] @@ -2462,6 +2973,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +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 1.0.94", + "quote 1.0.40", + "syn 2.0.117", +] [[package]] name = "zk_evm" @@ -2481,11 +3006,15 @@ dependencies = [ name = "zk_evm_abstractions" version = "0.153.10" dependencies = [ + "airbender-crypto", "anyhow", + "cfg-if", "hex", "num_enum", + "quickcheck", "serde", "static_assertions", + "tiny-keccak 2.0.2", "zkevm_opcode_defs", ] @@ -2515,9 +3044,9 @@ dependencies = [ "boojum", "derivative", "hex", - "itertools", + "itertools 0.10.5", "lazy_static", - "rand", + "rand 0.8.5", "seq-macro", "serde", "serde_json", @@ -2557,7 +3086,7 @@ dependencies = [ "ethabi", "hex", "indicatif", - "rand", + "rand 0.8.5", "rayon", "regex", "serde", @@ -2586,7 +3115,7 @@ dependencies = [ "hex", "lazy_static", "num_cpus", - "rand", + "rand 0.8.5", "rand_chacha", "rand_xorshift", "serde", @@ -2615,7 +3144,7 @@ checksum = "5bc485f5d7a16a83bebde0a083f8d27d94001c58485fbf3b12a21ed9ad3aacc0" dependencies = [ "byteorder", "hex", - "rand", + "rand 0.8.5", "rand_xorshift", "serde", "zksync_ff_derive", @@ -2644,7 +3173,7 @@ dependencies = [ "derivative", "hex", "once_cell", - "rand", + "rand 0.8.5", "rayon", "serde", "serde_json", @@ -2660,7 +3189,7 @@ checksum = "6e1becfb74e11f67afb73366c2f57412feab53581da029e6107719424b332b8b" dependencies = [ "byteorder", "cfg-if", - "rand", + "rand 0.8.5", "rand_xorshift", "serde", "zksync_ff", diff --git a/Cargo.toml b/Cargo.toml index d4f71ec7..825e914c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,10 @@ categories = ["cryptography"] circuit_definitions = { version = "=0.153.10", path = "crates/circuit_definitions" } circuit_encodings = { version = "=0.153.10", path = "crates/circuit_encodings" } circuit_sequencer_api = { version = "=0.153.10", path = "crates/circuit_sequencer_api" } +cfg-if = "1" kzg = { version = "=0.153.10", path = "crates/kzg", package = "zksync_kzg" } +quickcheck = "1" +tiny-keccak = "2" zk_evm = { version = "=0.153.10", path = "crates/zk_evm" } zk_evm_abstractions = { version = "=0.153.10", path = "crates/zk_evm_abstractions" } zkevm_circuits = { version = "=0.153.10", path = "crates/zkevm_circuits" } @@ -25,6 +28,9 @@ zkevm_opcode_defs = { version = "=0.153.10", path = "crates/zkevm_opcode_defs" } zkevm_test_harness = { version = "=0.153.10", path = "crates/zkevm_test_harness" } zkevm-assembly = { version = "=0.153.10", path = "crates/zkEVM-assembly" } +# TODO: Pin to a specific commit once `airbender-platform` is stablilized. +airbender-crypto = { git = "https://github.com/matter-labs/airbender-platform", branch = "main" } + # `zksync-crypto` workspace dependencies snark_wrapper = "=0.32.10" bellman = { package = "zksync_bellman", version = "=0.32.10" } diff --git a/crates/zk_evm_abstractions/Cargo.toml b/crates/zk_evm_abstractions/Cargo.toml index d59d6716..a6e0bf8d 100644 --- a/crates/zk_evm_abstractions/Cargo.toml +++ b/crates/zk_evm_abstractions/Cargo.toml @@ -16,9 +16,22 @@ zkevm_opcode_defs.workspace = true # "External" dependencies anyhow = "1.0" +cfg-if.workspace = true serde = { version = "1", features = ["derive"] } static_assertions = "1" num_enum = "0.6" + +[target.'cfg(target_arch = "riscv32")'.dependencies] +airbender-crypto = { workspace = true, optional = true, features = ["proving"] } + +[target.'cfg(not(target_arch = "riscv32"))'.dependencies] +airbender-crypto = { workspace = true, optional = true } + +[features] +airbender-precompile-delegations = ["dep:airbender-crypto"] + [dev-dependencies] hex = "0.4" +quickcheck.workspace = true +tiny-keccak = { workspace = true, features = ["keccak"] } diff --git a/crates/zk_evm_abstractions/src/precompiles/ecadd.rs b/crates/zk_evm_abstractions/src/precompiles/ecadd.rs deleted file mode 100644 index cbeb4261..00000000 --- a/crates/zk_evm_abstractions/src/precompiles/ecadd.rs +++ /dev/null @@ -1,489 +0,0 @@ -use anyhow::{Error, Result}; -use zkevm_opcode_defs::bn254::bn256::{Fq, G1Affine}; -use zkevm_opcode_defs::bn254::ff::PrimeField; -use zkevm_opcode_defs::bn254::{CurveAffine, CurveProjective}; -use zkevm_opcode_defs::ethereum_types::U256; -pub use zkevm_opcode_defs::sha2::Digest; - -use crate::utils::bn254::{point_to_u256_tuple, validate_values_in_field, ECPointCoordinates}; - -use super::*; - -// NOTE: We need x1, y1, x2, y2: four coordinates of two points -pub const MEMORY_READS_PER_CYCLE: usize = 4; -// NOTE: We need to specify the result of the addition and the status of the operation -pub const MEMORY_WRITES_PER_CYCLE: usize = 3; - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ECAddRoundWitness { - pub new_request: LogQuery, - pub reads: [MemoryQuery; MEMORY_READS_PER_CYCLE], - pub writes: [MemoryQuery; MEMORY_WRITES_PER_CYCLE], -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ECAddPrecompile; - -impl Precompile for ECAddPrecompile { - type CycleWitness = ECAddRoundWitness; - - fn execute_precompile( - &mut self, - monotonic_cycle_counter: u32, - query: LogQuery, - memory: &mut M, - ) -> ( - usize, - Option<(Vec, Vec, Vec)>, - ) { - const NUM_ROUNDS: usize = 1; - - // read the parameters - let precompile_call_params = query; - let params = precompile_abi_in_log(precompile_call_params); - let timestamp_to_read = precompile_call_params.timestamp; - let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); // our default timestamping agreement - - let mut current_read_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_read), - index: MemoryIndex(params.input_memory_offset), - }; - - // we assume that we have - // - x1 as U256 as a first coordinate of the first point (32 bytes) - // - y1 as U256 as a second coordinate of the first point (32 bytes) - // - x2 as U256 as a first coordinate of the second point (32 bytes) - // - y2 as U256 as a second coordinate of the second point (32 bytes) - - // we do 7 queries per precompile - let mut read_history = if B { - Vec::with_capacity(MEMORY_READS_PER_CYCLE) - } else { - vec![] - }; - let mut write_history = if B { - Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) - } else { - vec![] - }; - - let mut round_witness = ECAddRoundWitness { - new_request: precompile_call_params, - reads: [MemoryQuery::empty(); MEMORY_READS_PER_CYCLE], - writes: [MemoryQuery::empty(); MEMORY_WRITES_PER_CYCLE], - }; - - let mut read_idx = 0; - - let x1_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let x1_query = memory.execute_partial_query(monotonic_cycle_counter, x1_query); - let x1_value = x1_query.value; - if B { - round_witness.reads[read_idx] = x1_query; - read_idx += 1; - read_history.push(x1_query); - } - - current_read_location.index.0 += 1; - let y1_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let y1_query = memory.execute_partial_query(monotonic_cycle_counter, y1_query); - let y1_value = y1_query.value; - if B { - round_witness.reads[read_idx] = y1_query; - read_idx += 1; - read_history.push(y1_query); - } - - current_read_location.index.0 += 1; - let x2_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let x2_query = memory.execute_partial_query(monotonic_cycle_counter, x2_query); - let x2_value = x2_query.value; - if B { - round_witness.reads[read_idx] = x2_query; - read_idx += 1; - read_history.push(x2_query); - } - - current_read_location.index.0 += 1; - let y2_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let y2_query = memory.execute_partial_query(monotonic_cycle_counter, y2_query); - let y2_value = y2_query.value; - if B { - round_witness.reads[read_idx] = y2_query; - read_history.push(y2_query); - } - - // Performing addition - let points_sum = ecadd_inner((x1_value, y1_value), (x2_value, y2_value)); - - if let Ok((x, y)) = points_sum { - let mut write_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_write), - index: MemoryIndex(params.output_memory_offset), - }; - - let ok_marker = U256::one(); - let ok_or_err_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: ok_marker, - value_is_pointer: false, - rw_flag: true, - }; - let ok_or_err_query = - memory.execute_partial_query(monotonic_cycle_counter, ok_or_err_query); - - // Writing resultant x coordinate - write_location.index.0 += 1; - - let x_result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: x, - value_is_pointer: false, - rw_flag: true, - }; - let x_result_query = - memory.execute_partial_query(monotonic_cycle_counter, x_result_query); - - // Writing resultant y coordinate - write_location.index.0 += 1; - - let y_result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: y, - value_is_pointer: false, - rw_flag: true, - }; - let y_result_query = - memory.execute_partial_query(monotonic_cycle_counter, y_result_query); - - if B { - round_witness.writes[0] = ok_or_err_query; - round_witness.writes[1] = x_result_query; - round_witness.writes[2] = y_result_query; - write_history.push(ok_or_err_query); - write_history.push(x_result_query); - write_history.push(y_result_query); - } - } else { - let mut write_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_write), - index: MemoryIndex(params.output_memory_offset), - }; - - let err_marker = U256::zero(); - let ok_or_err_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: err_marker, - value_is_pointer: false, - rw_flag: true, - }; - let ok_or_err_query = - memory.execute_partial_query(monotonic_cycle_counter, ok_or_err_query); - - write_location.index.0 += 1; - let empty_result = U256::zero(); - let x_result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: empty_result, - value_is_pointer: false, - rw_flag: true, - }; - let x_result_query = - memory.execute_partial_query(monotonic_cycle_counter, x_result_query); - - write_location.index.0 += 1; - - let y_result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: empty_result, - value_is_pointer: false, - rw_flag: true, - }; - let y_result_query = - memory.execute_partial_query(monotonic_cycle_counter, y_result_query); - - if B { - round_witness.writes[0] = ok_or_err_query; - round_witness.writes[1] = x_result_query; - round_witness.writes[2] = y_result_query; - write_history.push(ok_or_err_query); - write_history.push(x_result_query); - write_history.push(y_result_query); - } - } - - let witness = if B { - Some((read_history, write_history, vec![round_witness])) - } else { - None - }; - - (NUM_ROUNDS, witness) - } -} - -/// This function adds two points (x1,y1) and (x2,y2) on the BN254 curve. -/// It returns the result as a G1Affine point. -/// -/// If the points are not on the curve or coordinates are not valid field elements, -/// the function will return an error. -pub fn ecadd_inner( - (x1, y1): ECPointCoordinates, - (x2, y2): ECPointCoordinates, -) -> Result { - if !validate_values_in_field(&[ - &x1.to_string(), - &y1.to_string(), - &x2.to_string(), - &y2.to_string(), - ]) { - return Err(Error::msg("invalid values")); - } - - // Converting coordinates to the finite field format - // and validating that the conversion is successful - let x1_field = Fq::from_str(x1.to_string().as_str()).ok_or(Error::msg("invalid x1"))?; - let y1_field = Fq::from_str(y1.to_string().as_str()).ok_or(Error::msg("invalid y1"))?; - let x2_field = Fq::from_str(x2.to_string().as_str()).ok_or(Error::msg("invalid x2"))?; - let y2_field = Fq::from_str(y2.to_string().as_str()).ok_or(Error::msg("invalid y2"))?; - - // If one of the points is zero, then both coordinates are zero, - // which aligns with the from_xy_checked method implementation. - // However, if some point does not lie on the curve, the method will return an error. - let point_1 = G1Affine::from_xy_checked(x1_field, y1_field)?; - let point_2 = G1Affine::from_xy_checked(x2_field, y2_field)?; - - let mut point_1_projective = point_1.into_projective(); - point_1_projective.add_assign_mixed(&point_2); - - let point_1 = point_1_projective.into_affine(); - let coordinates = point_to_u256_tuple(point_1); - Ok(coordinates) -} - -pub fn ecadd_function( - monotonic_cycle_counter: u32, - precompile_call_params: LogQuery, - memory: &mut M, -) -> ( - usize, - Option<(Vec, Vec, Vec)>, -) { - let mut processor = ECAddPrecompile::; - processor.execute_precompile(monotonic_cycle_counter, precompile_call_params, memory) -} - -#[cfg(test)] -pub mod tests { - /// Tests the correctness of the `ecadd_inner` function for a specified point - /// inside the test. - #[test] - fn test_ecadd_inner_correctness() { - use super::*; - - // Got: - let x1 = U256::from_str_radix( - "0x1148f79e53544582d22e5071480ae679d0b9df89d69e881f611e8381384ed1ad", - 16, - ) - .unwrap(); - let y1 = U256::from_str_radix( - "0xbac10178d2cd8aa9b4af903461b9f1666c219cdfeb2bb5e0cd7cd6486a32a6d", - 16, - ) - .unwrap(); - let x2 = U256::from_str_radix( - "0x251edb9081aba0cb29a45e4565ab2a2136750be5c893000e35e031ee123889e8", - 16, - ) - .unwrap(); - let y2 = U256::from_str_radix( - "0x24a972b009ad5986a7e14781d4e0c2d11aff281004712470811ec9b4fcb7c569", - 16, - ) - .unwrap(); - let (x, y) = ecadd_inner((x1, y1), (x2, y2)).unwrap(); - - // Expected: - let expected_x = U256::from_str_radix( - "16722044054529980026630802318818607593549086552476606668453035265973506741708", - 10, - ) - .unwrap(); - let expected_y = U256::from_str_radix( - "5777135421494458653665242593020841953920930780504228016288089286576416057645", - 10, - ) - .unwrap(); - - // Validation: - assert_eq!(x, expected_x, "x coordinates are not equal"); - assert_eq!(y, expected_y, "y coordinates are not equal"); - } - - /// Tests the correctness of the `ecadd_inner` function when given - /// two opposite points. - #[test] - fn test_ecadd_inner_point_at_infinity_1() { - use super::*; - - // Got: - let x1 = U256::from_str_radix( - "0x1148f79e53544582d22e5071480ae679d0b9df89d69e881f611e8381384ed1ad", - 16, - ) - .unwrap(); - let y1 = U256::from_str_radix( - "0xbac10178d2cd8aa9b4af903461b9f1666c219cdfeb2bb5e0cd7cd6486a32a6d", - 16, - ) - .unwrap(); - let x2 = x1.clone(); - // y2 = -y1 in Fr - let y2 = U256::from_str_radix( - "0x24b83e5b5404c77f1d054cb33b65b94730bf50c369bf0f2f2f48beb251d9d2da", - 16, - ) - .unwrap(); - let (x, y) = ecadd_inner((x1, y1), (x2, y2)).unwrap(); - - // Expected: - let expected_x = U256::zero(); - let expected_y = U256::zero(); - - // Validation: - assert_eq!(x, expected_x, "x coordinates are not equal"); - assert_eq!(y, expected_y, "y coordinates are not equal"); - } - - /// Tests the correctness of the `ecadd_inner` function when given - /// two points at infinity - #[test] - fn test_ecadd_inner_point_at_infinity_2() { - use super::*; - - // Got: - let zero = U256::zero(); - let (x, y) = ecadd_inner((zero, zero), (zero, zero)).unwrap(); - - // Expected: - let expected_x = U256::zero(); - let expected_y = U256::zero(); - - // Validation: - assert_eq!(x, expected_x, "x coordinates are not equal"); - assert_eq!(y, expected_y, "y coordinates are not equal"); - } - - /// Tests the correctness of the `ecadd_inner` function for a specified point - /// taken from https://www.evm.codes/precompiled#0x06 - #[test] - fn test_ecadd_inner_correctness_evm_codes() { - use super::*; - - // Got: - let x1 = U256::from_str_radix("1", 10).unwrap(); - let y1 = U256::from_str_radix("2", 10).unwrap(); - let x2 = U256::from_str_radix("1", 10).unwrap(); - let y2 = U256::from_str_radix("2", 10).unwrap(); - let (x, y) = ecadd_inner((x1, y1), (x2, y2)).unwrap(); - - // Expected: - let expected_x = U256::from_str_radix( - "1368015179489954701390400359078579693043519447331113978918064868415326638035", - 10, - ) - .unwrap(); - let expected_y = U256::from_str_radix( - "9918110051302171585080402603319702774565515993150576347155970296011118125764", - 10, - ) - .unwrap(); - - // Validation: - assert_eq!(x, expected_x, "x coordinates are not equal"); - assert_eq!(y, expected_y, "y coordinates are not equal"); - } - - /// Tests that the function does not allow point (x1, y1) that does not lie on the curve. - #[test] - #[should_panic] - fn test_ecadd_inner_invalid_x1y1() { - use super::*; - - // (x1, y1) does not lie on the curve - let x1 = U256::from_str_radix("1", 10).unwrap(); - let y1 = U256::from_str_radix("3", 10).unwrap(); - let x2 = U256::from_str_radix( - "0x251edb9081aba0cb29a45e4565ab2a2136750be5c893000e35e031ee123889e8", - 16, - ) - .unwrap(); - let y2 = U256::from_str_radix( - "0x24a972b009ad5986a7e14781d4e0c2d11aff281004712470811ec9b4fcb7c569", - 16, - ) - .unwrap(); - - // This should panic: - let _ = ecadd_inner((x1, y1), (x2, y2)).unwrap(); - } - - /// Tests that the function does not allow point (x2, y2) that does not lie on the curve. - #[test] - #[should_panic] - fn test_ecadd_inner_invalid_x2y2() { - use super::*; - - let x1 = U256::from_str_radix( - "0x1148f79e53544582d22e5071480ae679d0b9df89d69e881f611e8381384ed1ad", - 10, - ) - .unwrap(); - let y1 = U256::from_str_radix( - "0xbac10178d2cd8aa9b4af903461b9f1666c219cdfeb2bb5e0cd7cd6486a32a6d", - 10, - ) - .unwrap(); - - // (x2, y2) does not lie on the curve - let x2 = U256::from_str_radix("1", 16).unwrap(); - let y2 = U256::from_str_radix("10", 16).unwrap(); - - // This should panic: - let _ = ecadd_inner((x1, y1), (x2, y2)).unwrap(); - } -} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecadd/airbender_backend.rs b/crates/zk_evm_abstractions/src/precompiles/ecadd/airbender_backend.rs new file mode 100644 index 00000000..acfabd8e --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecadd/airbender_backend.rs @@ -0,0 +1,39 @@ +use airbender_crypto::ark_ec::{AffineRepr, CurveGroup}; +use anyhow::{Error, Result}; + +use crate::utils::airbender_bn254::{airbender_g1_from_coordinates, airbender_point_to_u256_tuple}; +use crate::utils::bn254::{validate_values_in_field, ECPointCoordinates}; + +use super::ECAddBackend; + +// ============================================================================== +// Delegated Backend +// ============================================================================== +// +// Delegated execution uses Airbender's BN254 implementation, but the caller still +// expects the same `(ok, x, y)` encoding and witness shape as the legacy path. +pub(super) struct DelegatedECAddBackend; + +impl ECAddBackend for DelegatedECAddBackend { + fn add( + (x1, y1): ECPointCoordinates, + (x2, y2): ECPointCoordinates, + ) -> Result { + if !validate_values_in_field(&[ + &x1.to_string(), + &y1.to_string(), + &x2.to_string(), + &y2.to_string(), + ]) { + return Err(Error::msg("invalid values")); + } + + let point_1 = airbender_g1_from_coordinates((x1, y1), "invalid x", "invalid y")?; + let point_2 = airbender_g1_from_coordinates((x2, y2), "invalid x", "invalid y")?; + + let mut sum = point_1.into_group(); + sum += point_2; + + Ok(airbender_point_to_u256_tuple(sum.into_affine())) + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecadd/legacy_backend.rs b/crates/zk_evm_abstractions/src/precompiles/ecadd/legacy_backend.rs new file mode 100644 index 00000000..7def494c --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecadd/legacy_backend.rs @@ -0,0 +1,45 @@ +use anyhow::{Error, Result}; +use zkevm_opcode_defs::bn254::bn256::{Fq, G1Affine}; +use zkevm_opcode_defs::bn254::ff::PrimeField; +use zkevm_opcode_defs::bn254::{CurveAffine, CurveProjective}; + +use crate::utils::bn254::{point_to_u256_tuple, validate_values_in_field, ECPointCoordinates}; + +use super::ECAddBackend; + +// ============================================================================== +// Legacy Backend +// ============================================================================== +// +// The legacy implementation uses the in-tree BN254 arithmetic and remains the +// reference behavior for both the default build and the delegated differential tests. +pub(super) struct LegacyECAddBackend; + +impl ECAddBackend for LegacyECAddBackend { + fn add( + (x1, y1): ECPointCoordinates, + (x2, y2): ECPointCoordinates, + ) -> Result { + if !validate_values_in_field(&[ + &x1.to_string(), + &y1.to_string(), + &x2.to_string(), + &y2.to_string(), + ]) { + return Err(Error::msg("invalid values")); + } + + let x1_field = Fq::from_str(x1.to_string().as_str()).ok_or(Error::msg("invalid x1"))?; + let y1_field = Fq::from_str(y1.to_string().as_str()).ok_or(Error::msg("invalid y1"))?; + let x2_field = Fq::from_str(x2.to_string().as_str()).ok_or(Error::msg("invalid x2"))?; + let y2_field = Fq::from_str(y2.to_string().as_str()).ok_or(Error::msg("invalid y2"))?; + + let point_1 = G1Affine::from_xy_checked(x1_field, y1_field)?; + let point_2 = G1Affine::from_xy_checked(x2_field, y2_field)?; + + let mut point_1_projective = point_1.into_projective(); + point_1_projective.add_assign_mixed(&point_2); + + Ok(point_to_u256_tuple(point_1_projective.into_affine())) + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecadd/mod.rs b/crates/zk_evm_abstractions/src/precompiles/ecadd/mod.rs new file mode 100644 index 00000000..493f902f --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecadd/mod.rs @@ -0,0 +1,180 @@ +use anyhow::Result; +use cfg_if::cfg_if; +use zkevm_opcode_defs::ethereum_types::U256; + +use crate::utils::bn254::ECPointCoordinates; + +use super::*; + +#[cfg(feature = "airbender-precompile-delegations")] +mod airbender_backend; +#[cfg(any(not(feature = "airbender-precompile-delegations"), test))] +mod legacy_backend; +#[cfg(test)] +mod tests; + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use self::airbender_backend::DelegatedECAddBackend as ActiveECAddBackend; + } else { + use self::legacy_backend::LegacyECAddBackend as ActiveECAddBackend; + } +} + +// NOTE: We need x1, y1, x2, y2: four coordinates of two points. +pub const MEMORY_READS_PER_CYCLE: usize = 4; +// NOTE: We write the status marker plus the result coordinates. +pub const MEMORY_WRITES_PER_CYCLE: usize = 3; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ECAddRoundWitness { + pub new_request: LogQuery, + pub reads: [MemoryQuery; MEMORY_READS_PER_CYCLE], + pub writes: [MemoryQuery; MEMORY_WRITES_PER_CYCLE], +} + +// ============================================================================== +// Backend Selection +// ============================================================================== +// +// The memory interface, witness recording, and success/error encoding are identical +// between the legacy and delegated implementations. The only meaningful variation +// is how the curve addition itself is performed, so the shared executor delegates +// just that piece to a backend trait. +trait ECAddBackend { + fn add(point_1: ECPointCoordinates, point_2: ECPointCoordinates) -> Result; +} + +fn execute_ecadd_precompile( + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<(Vec, Vec, Vec)>, +) { + const NUM_ROUNDS: usize = 1; + + let precompile_call_params = query; + let params = precompile_abi_in_log(precompile_call_params); + let timestamp_to_read = precompile_call_params.timestamp; + let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); + + let mut current_read_location = MemoryLocation { + memory_type: MemoryType::Heap, + page: MemoryPage(params.memory_page_to_read), + index: MemoryIndex(params.input_memory_offset), + }; + + let mut read_history = if B { + Vec::with_capacity(MEMORY_READS_PER_CYCLE) + } else { + vec![] + }; + let mut write_history = if B { + Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) + } else { + vec![] + }; + + let mut round_witness = ECAddRoundWitness { + new_request: precompile_call_params, + reads: [MemoryQuery::empty(); MEMORY_READS_PER_CYCLE], + writes: [MemoryQuery::empty(); MEMORY_WRITES_PER_CYCLE], + }; + + let mut input_words = [U256::zero(); MEMORY_READS_PER_CYCLE]; + for (idx, input_word) in input_words.iter_mut().enumerate() { + let read_query = MemoryQuery { + timestamp: timestamp_to_read, + location: current_read_location, + value: U256::zero(), + value_is_pointer: false, + rw_flag: false, + }; + let read_query = memory.execute_partial_query(monotonic_cycle_counter, read_query); + *input_word = read_query.value; + + if B { + round_witness.reads[idx] = read_query; + read_history.push(read_query); + } + + current_read_location.index.0 += 1; + } + + let result = Backend::add( + (input_words[0], input_words[1]), + (input_words[2], input_words[3]), + ); + let output_values = match result { + Ok((x, y)) => [U256::one(), x, y], + Err(_) => [U256::zero(), U256::zero(), U256::zero()], + }; + + let mut write_location = MemoryLocation { + memory_type: MemoryType::Heap, + page: MemoryPage(params.memory_page_to_write), + index: MemoryIndex(params.output_memory_offset), + }; + + for (idx, value) in output_values.into_iter().enumerate() { + let write_query = MemoryQuery { + timestamp: timestamp_to_write, + location: write_location, + value, + value_is_pointer: false, + rw_flag: true, + }; + let write_query = memory.execute_partial_query(monotonic_cycle_counter, write_query); + + if B { + round_witness.writes[idx] = write_query; + write_history.push(write_query); + } + + write_location.index.0 += 1; + } + + let witness = if B { + Some((read_history, write_history, vec![round_witness])) + } else { + None + }; + + (NUM_ROUNDS, witness) +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ECAddPrecompile; + +impl Precompile for ECAddPrecompile { + type CycleWitness = ECAddRoundWitness; + + fn execute_precompile( + &mut self, + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, + ) -> ( + usize, + Option<(Vec, Vec, Vec)>, + ) { + execute_ecadd_precompile::(monotonic_cycle_counter, query, memory) + } +} + +pub fn ecadd_function( + monotonic_cycle_counter: u32, + precompile_call_params: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<(Vec, Vec, Vec)>, +) { + execute_ecadd_precompile::( + monotonic_cycle_counter, + precompile_call_params, + memory, + ) +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecadd/tests/mod.rs b/crates/zk_evm_abstractions/src/precompiles/ecadd/tests/mod.rs new file mode 100644 index 00000000..cbc315dd --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecadd/tests/mod.rs @@ -0,0 +1,43 @@ +use cfg_if::cfg_if; + +use super::legacy_backend::LegacyECAddBackend; + +mod utils; + +use self::utils::{assert_backend_matches_case, deterministic_ecadd_cases}; + +#[test] +fn legacy_backend_matches_static_vectors() { + for case in deterministic_ecadd_cases() { + assert_backend_matches_case::(&case); + } +} + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use super::airbender_backend::DelegatedECAddBackend; + use quickcheck::QuickCheck; + use self::utils::{legacy_and_delegated_match_for_scalars, QUICKCHECK_NUM_CASES}; + + #[test] + fn delegated_backend_matches_static_vectors() { + for case in deterministic_ecadd_cases() { + assert_backend_matches_case::(&case); + } + } + + #[test] + fn delegated_backend_matches_legacy_quickcheck() { + fn property(left_scalar: u64, right_scalar: u64) -> bool { + legacy_and_delegated_match_for_scalars::< + LegacyECAddBackend, + DelegatedECAddBackend, + >(left_scalar, right_scalar) + } + + QuickCheck::new() + .tests(QUICKCHECK_NUM_CASES) + .quickcheck(property as fn(u64, u64) -> bool); + } + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecadd/tests/utils.rs b/crates/zk_evm_abstractions/src/precompiles/ecadd/tests/utils.rs new file mode 100644 index 00000000..fdb80f69 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecadd/tests/utils.rs @@ -0,0 +1,177 @@ +use anyhow::Result; +use cfg_if::cfg_if; +use zkevm_opcode_defs::ethereum_types::U256; + +use crate::utils::bn254::ECPointCoordinates; + +use super::super::ECAddBackend; + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use zkevm_opcode_defs::bn254::bn256::{Fr, G1Affine}; + use zkevm_opcode_defs::bn254::ff::PrimeField; + use zkevm_opcode_defs::bn254::{CurveAffine, CurveProjective}; + + use crate::utils::bn254::point_to_u256_tuple; + + pub(super) const QUICKCHECK_NUM_CASES: u64 = 256; + } +} + +pub(super) struct DeterministicECAddCase { + name: &'static str, + point_1: ECPointCoordinates, + point_2: ECPointCoordinates, + expected: Result, +} + +pub(super) fn deterministic_ecadd_cases() -> Vec { + vec![ + DeterministicECAddCase { + name: "custom-valid-point", + point_1: ( + u256_from_hex("1148f79e53544582d22e5071480ae679d0b9df89d69e881f611e8381384ed1ad"), + u256_from_hex("0bac10178d2cd8aa9b4af903461b9f1666c219cdfeb2bb5e0cd7cd6486a32a6d"), + ), + point_2: ( + u256_from_hex("251edb9081aba0cb29a45e4565ab2a2136750be5c893000e35e031ee123889e8"), + u256_from_hex("24a972b009ad5986a7e14781d4e0c2d11aff281004712470811ec9b4fcb7c569"), + ), + expected: Ok(( + u256_from_dec( + "16722044054529980026630802318818607593549086552476606668453035265973506741708", + ), + u256_from_dec( + "5777135421494458653665242593020841953920930780504228016288089286576416057645", + ), + )), + }, + DeterministicECAddCase { + name: "evm-codes-generator-doubling", + point_1: (u256_from_dec("1"), u256_from_dec("2")), + point_2: (u256_from_dec("1"), u256_from_dec("2")), + expected: Ok(( + u256_from_dec( + "1368015179489954701390400359078579693043519447331113978918064868415326638035", + ), + u256_from_dec( + "9918110051302171585080402603319702774565515993150576347155970296011118125764", + ), + )), + }, + DeterministicECAddCase { + name: "opposite-points-cancel", + point_1: ( + u256_from_hex("1148f79e53544582d22e5071480ae679d0b9df89d69e881f611e8381384ed1ad"), + u256_from_hex("0bac10178d2cd8aa9b4af903461b9f1666c219cdfeb2bb5e0cd7cd6486a32a6d"), + ), + point_2: ( + u256_from_hex("1148f79e53544582d22e5071480ae679d0b9df89d69e881f611e8381384ed1ad"), + u256_from_hex("24b83e5b5404c77f1d054cb33b65b94730bf50c369bf0f2f2f48beb251d9d2da"), + ), + expected: Ok((U256::zero(), U256::zero())), + }, + DeterministicECAddCase { + name: "infinity-plus-infinity", + point_1: (U256::zero(), U256::zero()), + point_2: (U256::zero(), U256::zero()), + expected: Ok((U256::zero(), U256::zero())), + }, + DeterministicECAddCase { + name: "infinity-plus-generator", + point_1: (U256::zero(), U256::zero()), + point_2: (u256_from_dec("1"), u256_from_dec("2")), + expected: Ok((u256_from_dec("1"), u256_from_dec("2"))), + }, + DeterministicECAddCase { + name: "invalid-first-point", + point_1: (u256_from_dec("1"), u256_from_dec("3")), + point_2: ( + u256_from_hex("251edb9081aba0cb29a45e4565ab2a2136750be5c893000e35e031ee123889e8"), + u256_from_hex("24a972b009ad5986a7e14781d4e0c2d11aff281004712470811ec9b4fcb7c569"), + ), + expected: Err(()), + }, + DeterministicECAddCase { + name: "invalid-second-point", + point_1: ( + u256_from_hex("1148f79e53544582d22e5071480ae679d0b9df89d69e881f611e8381384ed1ad"), + u256_from_hex("0bac10178d2cd8aa9b4af903461b9f1666c219cdfeb2bb5e0cd7cd6486a32a6d"), + ), + point_2: (u256_from_dec("1"), u256_from_dec("16")), + expected: Err(()), + }, + ] +} + +pub(super) fn assert_backend_matches_case(case: &DeterministicECAddCase) { + let actual = Backend::add(case.point_1, case.point_2); + + match (&case.expected, actual) { + (Ok(expected), Ok(actual)) => assert_eq!( + actual, *expected, + "backend must match static vector '{}'", + case.name, + ), + (Err(_), Err(_)) => {} + (Ok(_), Err(error)) => panic!( + "backend unexpectedly failed for vector '{}': {error}", + case.name, + ), + (Err(_), Ok(actual)) => panic!( + "backend unexpectedly succeeded for vector '{}': {actual:?}", + case.name, + ), + } +} + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + pub(super) fn legacy_and_delegated_match_for_scalars( + left_scalar: u64, + right_scalar: u64, + ) -> bool + where + Legacy: ECAddBackend, + Delegated: ECAddBackend, + { + let point_1 = point_from_scalar(left_scalar); + let point_2 = point_from_scalar(right_scalar); + + backend_results_match::(point_1, point_2) + } + + fn backend_results_match( + point_1: ECPointCoordinates, + point_2: ECPointCoordinates, + ) -> bool + where + Left: ECAddBackend, + Right: ECAddBackend, + { + let left = Left::add(point_1, point_2); + let right = Right::add(point_1, point_2); + + match (left, right) { + (Ok(left), Ok(right)) => left == right, + (Err(_), Err(_)) => true, + _ => false, + } + } + + fn point_from_scalar(scalar: u64) -> ECPointCoordinates { + let scalar = Fr::from_str(scalar.to_string().as_str()) + .expect("u64 scalar must map into the BN254 scalar field"); + let point = G1Affine::one().mul(scalar).into_affine(); + point_to_u256_tuple(point) + } + } +} + +fn u256_from_hex(hex: &str) -> U256 { + U256::from_str_radix(hex, 16).expect("hex vector must parse as U256") +} + +fn u256_from_dec(dec: &str) -> U256 { + U256::from_str_radix(dec, 10).expect("decimal vector must parse as U256") +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecmul.rs b/crates/zk_evm_abstractions/src/precompiles/ecmul.rs deleted file mode 100644 index f0964a51..00000000 --- a/crates/zk_evm_abstractions/src/precompiles/ecmul.rs +++ /dev/null @@ -1,471 +0,0 @@ -use std::str::FromStr; - -use anyhow::{Error, Result}; -use zkevm_opcode_defs::bn254::bn256::{Fq, Fr, G1Affine}; -use zkevm_opcode_defs::bn254::ff::PrimeField; -use zkevm_opcode_defs::bn254::{CurveAffine, CurveProjective}; -use zkevm_opcode_defs::ethereum_types::U256; -pub use zkevm_opcode_defs::sha2::Digest; - -use crate::utils::bn254::{point_to_u256_tuple, validate_values_in_field, ECPointCoordinates}; - -use super::*; - -// NOTE: We need x1, y1, and s: two coordinates of the point and the scalar -pub const MEMORY_READS_PER_CYCLE: usize = 3; -// NOTE: We need to specify the result of the multiplication and the status of the operation -pub const MEMORY_WRITES_PER_CYCLE: usize = 3; - -/// The order of the group of points on the BN254 curve. -pub const EC_GROUP_ORDER: &str = - "0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001"; - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ECMulRoundWitness { - pub new_request: LogQuery, - pub reads: [MemoryQuery; MEMORY_READS_PER_CYCLE], - pub writes: [MemoryQuery; MEMORY_WRITES_PER_CYCLE], -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ECMulPrecompile; - -impl Precompile for ECMulPrecompile { - type CycleWitness = ECMulRoundWitness; - - fn execute_precompile( - &mut self, - monotonic_cycle_counter: u32, - query: LogQuery, - memory: &mut M, - ) -> ( - usize, - Option<(Vec, Vec, Vec)>, - ) { - const NUM_ROUNDS: usize = 1; - - // read the parameters - let precompile_call_params = query; - let params = precompile_abi_in_log(precompile_call_params); - let timestamp_to_read = precompile_call_params.timestamp; - let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); // our default timestamping agreement - - let mut current_read_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_read), - index: MemoryIndex(params.input_memory_offset), - }; - - // we assume that we have - // - x1 as U256 as a first coordinate of the point (32 bytes) - // - y1 as U256 as a second coordinate of the point (32 bytes) - // - s as U256 as a scalar to multiply with (32 bytes) - - // we do 6 queries per precompile - let mut read_history = if B { - Vec::with_capacity(MEMORY_READS_PER_CYCLE) - } else { - vec![] - }; - let mut write_history = if B { - Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) - } else { - vec![] - }; - - let mut round_witness = ECMulRoundWitness { - new_request: precompile_call_params, - reads: [MemoryQuery::empty(); MEMORY_READS_PER_CYCLE], - writes: [MemoryQuery::empty(); MEMORY_WRITES_PER_CYCLE], - }; - - let mut read_idx = 0; - - let x1_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let x1_query = memory.execute_partial_query(monotonic_cycle_counter, x1_query); - let x1_value = x1_query.value; - if B { - round_witness.reads[read_idx] = x1_query; - read_idx += 1; - read_history.push(x1_query); - } - - current_read_location.index.0 += 1; - let y1_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let y1_query = memory.execute_partial_query(monotonic_cycle_counter, y1_query); - let y1_value = y1_query.value; - if B { - round_witness.reads[read_idx] = y1_query; - read_idx += 1; - read_history.push(y1_query); - } - - current_read_location.index.0 += 1; - let s_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let s_query = memory.execute_partial_query(monotonic_cycle_counter, s_query); - let s_value = s_query.value; - if B { - round_witness.reads[read_idx] = s_query; - read_history.push(s_query); - } - - // Performing multiplication - let point_multiplied = ecmul_inner((x1_value, y1_value), s_value); - - if let Ok((x, y)) = point_multiplied { - let mut write_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_write), - index: MemoryIndex(params.output_memory_offset), - }; - - // Marking that the operation was successful - let ok_marker = U256::one(); - let ok_or_err_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: ok_marker, - value_is_pointer: false, - rw_flag: true, - }; - let ok_or_err_query = - memory.execute_partial_query(monotonic_cycle_counter, ok_or_err_query); - - // Writing resultant x coordinate - write_location.index.0 += 1; - - let x_result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: x, - value_is_pointer: false, - rw_flag: true, - }; - let x_result_query = - memory.execute_partial_query(monotonic_cycle_counter, x_result_query); - - // Writing resultant y coordinate - write_location.index.0 += 1; - - let y_result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: y, - value_is_pointer: false, - rw_flag: true, - }; - let y_result_query = - memory.execute_partial_query(monotonic_cycle_counter, y_result_query); - - if B { - round_witness.writes[0] = ok_or_err_query; - round_witness.writes[1] = x_result_query; - round_witness.writes[2] = y_result_query; - write_history.push(ok_or_err_query); - write_history.push(x_result_query); - write_history.push(y_result_query); - } - } else { - let mut write_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_write), - index: MemoryIndex(params.output_memory_offset), - }; - - let err_marker = U256::zero(); - let ok_or_err_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: err_marker, - value_is_pointer: false, - rw_flag: true, - }; - let ok_or_err_query = - memory.execute_partial_query(monotonic_cycle_counter, ok_or_err_query); - - write_location.index.0 += 1; - let empty_result = U256::zero(); - let x_result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: empty_result, - value_is_pointer: false, - rw_flag: true, - }; - let x_result_query = - memory.execute_partial_query(monotonic_cycle_counter, x_result_query); - - write_location.index.0 += 1; - let empty_result = U256::zero(); - let y_result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: empty_result, - value_is_pointer: false, - rw_flag: true, - }; - let y_result_query = - memory.execute_partial_query(monotonic_cycle_counter, y_result_query); - - if B { - round_witness.writes[0] = ok_or_err_query; - round_witness.writes[1] = x_result_query; - round_witness.writes[2] = y_result_query; - write_history.push(ok_or_err_query); - write_history.push(x_result_query); - write_history.push(y_result_query); - } - } - - let witness = if B { - Some((read_history, write_history, vec![round_witness])) - } else { - None - }; - - (NUM_ROUNDS, witness) - } -} - -/// This function multiplies the point (x1,y1) by the scalar s on the BN254 curve. -/// It returns the result as a G1Affine point, represented by two U256. -/// -/// If the points are not on the curve, the function will return an error. -pub fn ecmul_inner((x1, y1): ECPointCoordinates, s: U256) -> Result { - if !validate_values_in_field(&[&x1.to_string(), &y1.to_string()]) { - return Err(Error::msg("invalid values")); - } - - // Converting coordinates to the finite field format - // and validating that the conversion is successful - let x1_field = Fq::from_str(x1.to_string().as_str()).ok_or(Error::msg("invalid x1"))?; - let y1_field = Fq::from_str(y1.to_string().as_str()).ok_or(Error::msg("invalid y1"))?; - - let u256_to_field = |u: U256| -> Fr { - // If the given uint256 is less than the order of the group r, we do not touch it. - // In rare cases where this scalar is indeed less than r, we subtract - // the order of the group from the scalar until it is less than r. - let group_order = U256::from_str(EC_GROUP_ORDER).unwrap(); - let mut u = u.clone(); - - // NOTE: Since 2**256 / r is approximately 5.29, we need max 6 subtractions. - // This still better than a division operation. - while u >= group_order { - u -= group_order; - } - - Fr::from_str(u.to_string().as_str()).unwrap() - }; - - let s_field = u256_to_field(s); - - // If one of the points is zero, then both coordinates are zero, - // which aligns with the from_xy_checked method implementation. - // However, if some point does not lie on the curve, the method will return an error. - let point_1 = G1Affine::from_xy_checked(x1_field, y1_field)?; - - let multiplied = point_1.mul(s_field).into_affine(); - let u256_tuple = point_to_u256_tuple(multiplied); - Ok(u256_tuple) -} - -pub fn ecmul_function( - monotonic_cycle_counter: u32, - precompile_call_params: LogQuery, - memory: &mut M, -) -> ( - usize, - Option<(Vec, Vec, Vec)>, -) { - let mut processor = ECMulPrecompile::; - processor.execute_precompile(monotonic_cycle_counter, precompile_call_params, memory) -} - -#[cfg(test)] -pub mod tests { - /// Tests the correctness of the `ecmul_inner` function for a specified point - /// and a scalar inside the test. - #[test] - fn test_ecmul_inner_correctness() { - use super::*; - - // Got: - let x1 = U256::from_str_radix( - "0x1148f79e53544582d22e5071480ae679d0b9df89d69e881f611e8381384ed1ad", - 16, - ) - .unwrap(); - let y1 = U256::from_str_radix( - "0xbac10178d2cd8aa9b4af903461b9f1666c219cdfeb2bb5e0cd7cd6486a32a6d", - 16, - ) - .unwrap(); - let s = U256::from_str_radix( - "0x15f0e77d431a6c4d21df6a71cdcb0b2eeba21fc1192bd9801b8cd8b7c763e115", - 16, - ) - .unwrap(); - let (x, y) = ecmul_inner((x1, y1), s).unwrap(); - - // Expected: - let expected_x = U256::from_str_radix( - "9941674825074992183128808489717167636392653540258056893654639521381088261704", - 10, - ) - .unwrap(); - let expected_y = U256::from_str_radix( - "8986289197266457569457494475222656986225227492679168701241837087965910154278", - 10, - ) - .unwrap(); - - // Validation: - assert_eq!(x, expected_x, "x coordinate is incorrect"); - assert_eq!(y, expected_y, "y coordinate is incorrect"); - } - - /// Tests the correctness of the `ecmul_inner` function for a specified point - /// taken from https://www.evm.codes/precompiled#0x07 - #[test] - fn test_ecmul_inner_correctness_evm_codes() { - use super::*; - - // Got: - let x1 = U256::from_str_radix("1", 10).unwrap(); - let y1 = U256::from_str_radix("2", 10).unwrap(); - let s = U256::from_str_radix("2", 10).unwrap(); - let (x, y) = ecmul_inner((x1, y1), s).unwrap(); - - // Expected: - let expected_x = U256::from_str_radix( - "1368015179489954701390400359078579693043519447331113978918064868415326638035", - 10, - ) - .unwrap(); - let expected_y = U256::from_str_radix( - "9918110051302171585080402603319702774565515993150576347155970296011118125764", - 10, - ) - .unwrap(); - - // Validation: - assert_eq!(x, expected_x, "x coordinate is incorrect"); - assert_eq!(y, expected_y, "y coordinate is incorrect"); - } - - /// Tests the correctness of the `ecmul_inner` function for a specified point - /// taken from https://www.evm.codes/precompiled#0x07 when the scalar - /// provided equals the group order. We expect to get the point at infinity. - #[test] - fn test_ecmul_inner_correctness_order_overflow_1() { - use super::*; - - // Got: - // Generator point, scalar is a group order - let x1 = U256::from_str_radix("1", 10).unwrap(); - let y1 = U256::from_str_radix("2", 10).unwrap(); - let s = U256::from_str_radix(EC_GROUP_ORDER, 16).unwrap(); - let (x, y) = ecmul_inner((x1, y1), s).unwrap(); - - // Expected: - // NOTE: Scalar is the group order, thus the result should be the point at infinity - let expected_x = U256::from_str_radix("0", 10).unwrap(); - let expected_y = U256::from_str_radix("0", 10).unwrap(); - - assert_eq!(x, expected_x, "x coordinate is incorrect"); - assert_eq!(y, expected_y, "y coordinate is incorrect"); - } - - /// Tests the correctness of the `ecmul_inner` function for a specified point - /// taken from https://www.evm.codes/precompiled#0x07 when the scalar - /// provided equals the 3*(group order). We expect to get the point at infinity. - /// Since 3*(group order) does not overflow u256, this is a valid test. - #[test] - fn test_ecmul_inner_correctness_order_overflow_2() { - use super::*; - - // Got: - // Generator point, scalar is 3*(group order) - let x1 = U256::from_str_radix("1", 10).unwrap(); - let y1 = U256::from_str_radix("2", 10).unwrap(); - let s = U256::from_str_radix( - "0x912ceb58a394e07d28f0d12384840917789bb8d96d2c51b3cba5e0bbd0000003", - 16, - ) - .unwrap(); - let (x, y) = ecmul_inner((x1, y1), s).unwrap(); - - // Expected: - // NOTE: Scalar is 3*(group order), thus the result should be the point at infinity - let expected_x = U256::from_str_radix("0", 10).unwrap(); - let expected_y = U256::from_str_radix("0", 10).unwrap(); - - assert_eq!(x, expected_x, "x coordinate is incorrect"); - assert_eq!(y, expected_y, "y coordinate is incorrect"); - } - - /// Tests the correctness of the `ecmul_inner` function for a specified point - /// taken from https://www.evm.codes/precompiled#0x07 when the scalar - /// provided equals the 5*(group order)+1. We expect to get the inputted point. - /// Since 5*(group order)+1 does not overflow u256, this is a valid test. - #[test] - fn test_ecmul_inner_correctness_order_overflow_3() { - use super::*; - - // Got: - // Generator point, scalar is 5*(group order)+1 - let x1 = U256::from_str_radix("1", 10).unwrap(); - let y1 = U256::from_str_radix("2", 10).unwrap(); - let s = U256::from_str_radix( - "0xf1f5883e65f820d099915c908786b9d1c903896a609f32d65369cbe3b0000006", - 16, - ) - .unwrap(); - let (x, y) = ecmul_inner((x1, y1), s).unwrap(); - - // Expected: - // NOTE: Scalar is 5*(group order)+1, thus the result should be (x1, y1) - let expected_x = x1.clone(); - let expected_y = y1.clone(); - - assert_eq!(x, expected_x, "x coordinate is incorrect"); - assert_eq!(y, expected_y, "y coordinate is incorrect"); - } - - /// Tests that the function does not allow to multiply by an invalid point. - #[test] - #[should_panic] - fn test_ecmul_invalid_point() { - use super::*; - - // (x1, y1) does not lie on the curve - let x1 = U256::from_str_radix("1", 10).unwrap(); - let y1 = U256::from_str_radix("10", 10).unwrap(); - let s = U256::from_str_radix( - "0x15f0e77d431a6c4d21df6a71cdcb0b2eeba21fc1192bd9801b8cd8b7c763e115", - 16, - ) - .unwrap(); - - // This should panic - let _ = ecmul_inner((x1, y1), s).unwrap(); - } -} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecmul/airbender_backend.rs b/crates/zk_evm_abstractions/src/precompiles/ecmul/airbender_backend.rs new file mode 100644 index 00000000..db6301c7 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecmul/airbender_backend.rs @@ -0,0 +1,30 @@ +use airbender_crypto::ark_ec::{AffineRepr, CurveGroup}; +use airbender_crypto::ark_ff::PrimeField as AirPrimeField; +use anyhow::{Error, Result}; +use zkevm_opcode_defs::ethereum_types::U256; + +use crate::utils::airbender_bn254::{ + airbender_fr_from_u256, airbender_g1_from_coordinates, airbender_point_to_u256_tuple, +}; +use crate::utils::bn254::{validate_values_in_field, ECPointCoordinates}; + +use super::{ECMulBackend, EC_GROUP_ORDER}; + +// ============================================================================== +// Delegated Backend +// ============================================================================== +pub(super) struct DelegatedECMulBackend; + +impl ECMulBackend for DelegatedECMulBackend { + fn mul((x1, y1): ECPointCoordinates, scalar: U256) -> Result { + if !validate_values_in_field(&[&x1.to_string(), &y1.to_string()]) { + return Err(Error::msg("invalid values")); + } + + let point = airbender_g1_from_coordinates((x1, y1), "invalid x1", "invalid y1")?; + let scalar = airbender_fr_from_u256(scalar, EC_GROUP_ORDER, "invalid scalar")?; + let multiplied = point.mul_bigint(scalar.into_bigint()).into_affine(); + + Ok(airbender_point_to_u256_tuple(multiplied)) + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecmul/legacy_backend.rs b/crates/zk_evm_abstractions/src/precompiles/ecmul/legacy_backend.rs new file mode 100644 index 00000000..d0628258 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecmul/legacy_backend.rs @@ -0,0 +1,46 @@ +use anyhow::{Error, Result}; +use zkevm_opcode_defs::bn254::bn256::{Fq, Fr, G1Affine}; +use zkevm_opcode_defs::bn254::ff::PrimeField; +use zkevm_opcode_defs::bn254::{CurveAffine, CurveProjective}; +use zkevm_opcode_defs::ethereum_types::U256; + +use crate::utils::bn254::{point_to_u256_tuple, validate_values_in_field, ECPointCoordinates}; + +use super::{ECMulBackend, EC_GROUP_ORDER}; + +// ============================================================================== +// Legacy Backend +// ============================================================================== +// +// The legacy backend preserves the existing BN254 multiplication semantics and +// serves as the oracle for delegated differential tests. +pub(super) struct LegacyECMulBackend; + +impl ECMulBackend for LegacyECMulBackend { + fn mul((x1, y1): ECPointCoordinates, s: U256) -> Result { + if !validate_values_in_field(&[&x1.to_string(), &y1.to_string()]) { + return Err(Error::msg("invalid values")); + } + + let x1_field = Fq::from_str(x1.to_string().as_str()).ok_or(Error::msg("invalid x1"))?; + let y1_field = Fq::from_str(y1.to_string().as_str()).ok_or(Error::msg("invalid y1"))?; + let s_field = u256_to_scalar(s); + + let point = G1Affine::from_xy_checked(x1_field, y1_field)?; + Ok(point_to_u256_tuple(point.mul(s_field).into_affine())) + } +} + +fn u256_to_scalar(value: U256) -> Fr { + let group_order = U256::from_str_radix(EC_GROUP_ORDER.trim_start_matches("0x"), 16) + .expect("group order constant must parse"); + let mut reduced = value; + + // NOTE: `2^256 / r` is about 5.29, so six subtractions are sufficient here. + while reduced >= group_order { + reduced -= group_order; + } + + Fr::from_str(reduced.to_string().as_str()) + .expect("reduced scalar must fit into the BN254 scalar field") +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecmul/mod.rs b/crates/zk_evm_abstractions/src/precompiles/ecmul/mod.rs new file mode 100644 index 00000000..1e928fd5 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecmul/mod.rs @@ -0,0 +1,173 @@ +use anyhow::Result; +use cfg_if::cfg_if; +use zkevm_opcode_defs::ethereum_types::U256; + +use crate::utils::bn254::ECPointCoordinates; + +use super::*; + +#[cfg(feature = "airbender-precompile-delegations")] +mod airbender_backend; +#[cfg(any(not(feature = "airbender-precompile-delegations"), test))] +mod legacy_backend; +#[cfg(test)] +mod tests; + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use self::airbender_backend::DelegatedECMulBackend as ActiveECMulBackend; + } else { + use self::legacy_backend::LegacyECMulBackend as ActiveECMulBackend; + } +} + +// NOTE: We need x1, y1, and s: two coordinates of the point and the scalar. +pub const MEMORY_READS_PER_CYCLE: usize = 3; +// NOTE: We write the status marker plus the result coordinates. +pub const MEMORY_WRITES_PER_CYCLE: usize = 3; + +/// The order of the group of points on the BN254 curve. +pub const EC_GROUP_ORDER: &str = + "0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001"; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ECMulRoundWitness { + pub new_request: LogQuery, + pub reads: [MemoryQuery; MEMORY_READS_PER_CYCLE], + pub writes: [MemoryQuery; MEMORY_WRITES_PER_CYCLE], +} + +trait ECMulBackend { + fn mul(point: ECPointCoordinates, scalar: U256) -> Result; +} + +fn execute_ecmul_precompile( + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<(Vec, Vec, Vec)>, +) { + const NUM_ROUNDS: usize = 1; + + let precompile_call_params = query; + let params = precompile_abi_in_log(precompile_call_params); + let timestamp_to_read = precompile_call_params.timestamp; + let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); + + let mut current_read_location = MemoryLocation { + memory_type: MemoryType::Heap, + page: MemoryPage(params.memory_page_to_read), + index: MemoryIndex(params.input_memory_offset), + }; + + let mut read_history = if B { + Vec::with_capacity(MEMORY_READS_PER_CYCLE) + } else { + vec![] + }; + let mut write_history = if B { + Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) + } else { + vec![] + }; + + let mut round_witness = ECMulRoundWitness { + new_request: precompile_call_params, + reads: [MemoryQuery::empty(); MEMORY_READS_PER_CYCLE], + writes: [MemoryQuery::empty(); MEMORY_WRITES_PER_CYCLE], + }; + + let mut input_words = [U256::zero(); MEMORY_READS_PER_CYCLE]; + for (idx, input_word) in input_words.iter_mut().enumerate() { + let read_query = MemoryQuery { + timestamp: timestamp_to_read, + location: current_read_location, + value: U256::zero(), + value_is_pointer: false, + rw_flag: false, + }; + let read_query = memory.execute_partial_query(monotonic_cycle_counter, read_query); + *input_word = read_query.value; + + if B { + round_witness.reads[idx] = read_query; + read_history.push(read_query); + } + + current_read_location.index.0 += 1; + } + + let result = Backend::mul((input_words[0], input_words[1]), input_words[2]); + let output_values = match result { + Ok((x, y)) => [U256::one(), x, y], + Err(_) => [U256::zero(), U256::zero(), U256::zero()], + }; + + let mut write_location = MemoryLocation { + memory_type: MemoryType::Heap, + page: MemoryPage(params.memory_page_to_write), + index: MemoryIndex(params.output_memory_offset), + }; + + for (idx, value) in output_values.into_iter().enumerate() { + let write_query = MemoryQuery { + timestamp: timestamp_to_write, + location: write_location, + value, + value_is_pointer: false, + rw_flag: true, + }; + let write_query = memory.execute_partial_query(monotonic_cycle_counter, write_query); + + if B { + round_witness.writes[idx] = write_query; + write_history.push(write_query); + } + + write_location.index.0 += 1; + } + + let witness = if B { + Some((read_history, write_history, vec![round_witness])) + } else { + None + }; + + (NUM_ROUNDS, witness) +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ECMulPrecompile; + +impl Precompile for ECMulPrecompile { + type CycleWitness = ECMulRoundWitness; + + fn execute_precompile( + &mut self, + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, + ) -> ( + usize, + Option<(Vec, Vec, Vec)>, + ) { + execute_ecmul_precompile::(monotonic_cycle_counter, query, memory) + } +} + +pub fn ecmul_function( + monotonic_cycle_counter: u32, + precompile_call_params: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<(Vec, Vec, Vec)>, +) { + execute_ecmul_precompile::( + monotonic_cycle_counter, + precompile_call_params, + memory, + ) +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecmul/tests/mod.rs b/crates/zk_evm_abstractions/src/precompiles/ecmul/tests/mod.rs new file mode 100644 index 00000000..ff17a242 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecmul/tests/mod.rs @@ -0,0 +1,43 @@ +use cfg_if::cfg_if; + +use super::legacy_backend::LegacyECMulBackend; + +mod utils; + +use self::utils::{assert_backend_matches_case, deterministic_ecmul_cases}; + +#[test] +fn legacy_backend_matches_static_vectors() { + for case in deterministic_ecmul_cases() { + assert_backend_matches_case::(&case); + } +} + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use quickcheck::QuickCheck; + use super::airbender_backend::DelegatedECMulBackend; + use self::utils::{legacy_and_delegated_match_for_scalars, QUICKCHECK_NUM_CASES}; + + #[test] + fn delegated_backend_matches_static_vectors() { + for case in deterministic_ecmul_cases() { + assert_backend_matches_case::(&case); + } + } + + #[test] + fn delegated_backend_matches_legacy_quickcheck() { + fn property(point_scalar: u64, multiplier_scalar: u64) -> bool { + legacy_and_delegated_match_for_scalars::< + LegacyECMulBackend, + DelegatedECMulBackend, + >(point_scalar, multiplier_scalar) + } + + QuickCheck::new() + .tests(QUICKCHECK_NUM_CASES) + .quickcheck(property as fn(u64, u64) -> bool); + } + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecmul/tests/utils.rs b/crates/zk_evm_abstractions/src/precompiles/ecmul/tests/utils.rs new file mode 100644 index 00000000..7c10742e --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecmul/tests/utils.rs @@ -0,0 +1,160 @@ +use cfg_if::cfg_if; +use zkevm_opcode_defs::ethereum_types::U256; + +use crate::utils::bn254::ECPointCoordinates; + +use super::super::{ECMulBackend, EC_GROUP_ORDER}; + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use zkevm_opcode_defs::bn254::bn256::{Fr, G1Affine}; + use zkevm_opcode_defs::bn254::ff::PrimeField; + use zkevm_opcode_defs::bn254::{CurveAffine, CurveProjective}; + + use crate::utils::bn254::point_to_u256_tuple; + + pub(super) const QUICKCHECK_NUM_CASES: u64 = 256; + } +} + +pub(super) struct DeterministicECMulCase { + name: &'static str, + point: ECPointCoordinates, + scalar: U256, + expected: Result, +} + +pub(super) fn deterministic_ecmul_cases() -> Vec { + vec![ + DeterministicECMulCase { + name: "custom-valid-point", + point: ( + u256_from_hex("1148f79e53544582d22e5071480ae679d0b9df89d69e881f611e8381384ed1ad"), + u256_from_hex("0bac10178d2cd8aa9b4af903461b9f1666c219cdfeb2bb5e0cd7cd6486a32a6d"), + ), + scalar: u256_from_hex( + "15f0e77d431a6c4d21df6a71cdcb0b2eeba21fc1192bd9801b8cd8b7c763e115", + ), + expected: Ok(( + u256_from_dec( + "9941674825074992183128808489717167636392653540258056893654639521381088261704", + ), + u256_from_dec( + "8986289197266457569457494475222656986225227492679168701241837087965910154278", + ), + )), + }, + DeterministicECMulCase { + name: "evm-codes-generator-double", + point: (u256_from_dec("1"), u256_from_dec("2")), + scalar: u256_from_dec("2"), + expected: Ok(( + u256_from_dec( + "1368015179489954701390400359078579693043519447331113978918064868415326638035", + ), + u256_from_dec( + "9918110051302171585080402603319702774565515993150576347155970296011118125764", + ), + )), + }, + DeterministicECMulCase { + name: "group-order-wraps-to-infinity", + point: (u256_from_dec("1"), u256_from_dec("2")), + scalar: u256_from_hex(EC_GROUP_ORDER.trim_start_matches("0x")), + expected: Ok((U256::zero(), U256::zero())), + }, + DeterministicECMulCase { + name: "three-group-orders-wrap-to-infinity", + point: (u256_from_dec("1"), u256_from_dec("2")), + scalar: u256_from_hex( + "912ceb58a394e07d28f0d12384840917789bb8d96d2c51b3cba5e0bbd0000003", + ), + expected: Ok((U256::zero(), U256::zero())), + }, + DeterministicECMulCase { + name: "five-group-orders-plus-one-reproduces-input", + point: (u256_from_dec("1"), u256_from_dec("2")), + scalar: u256_from_hex( + "f1f5883e65f820d099915c908786b9d1c903896a609f32d65369cbe3b0000006", + ), + expected: Ok((u256_from_dec("1"), u256_from_dec("2"))), + }, + DeterministicECMulCase { + name: "invalid-point", + point: (u256_from_dec("1"), u256_from_dec("10")), + scalar: u256_from_hex( + "15f0e77d431a6c4d21df6a71cdcb0b2eeba21fc1192bd9801b8cd8b7c763e115", + ), + expected: Err(()), + }, + ] +} + +pub(super) fn assert_backend_matches_case(case: &DeterministicECMulCase) { + let actual = Backend::mul(case.point, case.scalar); + + match (&case.expected, actual) { + (Ok(expected), Ok(actual)) => assert_eq!( + actual, *expected, + "backend must match static vector '{}'", + case.name, + ), + (Err(_), Err(_)) => {} + (Ok(_), Err(error)) => panic!( + "backend unexpectedly failed for vector '{}': {error}", + case.name, + ), + (Err(_), Ok(actual)) => panic!( + "backend unexpectedly succeeded for vector '{}': {actual:?}", + case.name, + ), + } +} + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + pub(super) fn legacy_and_delegated_match_for_scalars( + point_scalar: u64, + multiplier_scalar: u64, + ) -> bool + where + Legacy: ECMulBackend, + Delegated: ECMulBackend, + { + let point = point_from_scalar(point_scalar); + let scalar = U256::from(multiplier_scalar); + + backend_results_match::(point, scalar) + } + + fn backend_results_match(point: ECPointCoordinates, scalar: U256) -> bool + where + Left: ECMulBackend, + Right: ECMulBackend, + { + let left = Left::mul(point, scalar); + let right = Right::mul(point, scalar); + + match (left, right) { + (Ok(left), Ok(right)) => left == right, + (Err(_), Err(_)) => true, + _ => false, + } + } + + fn point_from_scalar(scalar: u64) -> ECPointCoordinates { + let scalar = Fr::from_str(scalar.to_string().as_str()) + .expect("u64 scalar must map into the BN254 scalar field"); + let point = G1Affine::one().mul(scalar).into_affine(); + point_to_u256_tuple(point) + } + } +} + +fn u256_from_hex(hex: &str) -> U256 { + U256::from_str_radix(hex, 16).expect("hex vector must parse as U256") +} + +fn u256_from_dec(dec: &str) -> U256 { + U256::from_str_radix(dec, 10).expect("decimal vector must parse as U256") +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecpairing.rs b/crates/zk_evm_abstractions/src/precompiles/ecpairing.rs deleted file mode 100644 index 3d653a3c..00000000 --- a/crates/zk_evm_abstractions/src/precompiles/ecpairing.rs +++ /dev/null @@ -1,684 +0,0 @@ -use anyhow::{Error, Result}; -use zkevm_opcode_defs::bn254::bn256::{ - self, Fq, Fq12, Fq2, G1Affine, G2Affine, FROBENIUS_COEFF_FQ6_C1, XI_TO_Q_MINUS_1_OVER_2, -}; -use zkevm_opcode_defs::bn254::ff::{Field, PrimeField}; -use zkevm_opcode_defs::bn254::{CurveAffine, CurveProjective}; -use zkevm_opcode_defs::ethereum_types::U256; -pub use zkevm_opcode_defs::sha2::Digest; - -use crate::utils::bn254::validate_values_in_field; - -use super::*; - -// NOTE: We need x1, y1, x2, y2, x3, y3: -pub const MEMORY_READS_PER_CYCLE: usize = 6; -// NOTE: We need to specify the result of the pairing and the status of the operation -pub const MEMORY_WRITES_PER_CYCLE: usize = 2; - -// x1, y1, x2, y2, x3, y3 -type EcPairingInputTuple = [U256; 6]; - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ECPairingRoundWitness { - pub new_request: Option, - pub reads: [MemoryQuery; MEMORY_READS_PER_CYCLE], - pub writes: Option<[MemoryQuery; MEMORY_WRITES_PER_CYCLE]>, -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ECPairingPrecompile; - -impl Precompile for ECPairingPrecompile { - type CycleWitness = ECPairingRoundWitness; - - fn execute_precompile( - &mut self, - monotonic_cycle_counter: u32, - query: LogQuery, - memory: &mut M, - ) -> ( - usize, - Option<(Vec, Vec, Vec)>, - ) { - // read the parameters - let precompile_call_params = query; - let params = precompile_abi_in_log(precompile_call_params); - let num_rounds = params.precompile_interpreted_data as usize; - let timestamp_to_read = precompile_call_params.timestamp; - let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); // our default timestamping agreement - - let mut current_read_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_read), - index: MemoryIndex(params.input_memory_offset), - }; - - let mut read_history = if B { - Vec::with_capacity(num_rounds * MEMORY_READS_PER_CYCLE) - } else { - vec![] - }; - let mut write_history = if B { - Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) - } else { - vec![] - }; - - let mut check_tuples = Vec::::with_capacity(num_rounds); - let mut witnesses = Vec::::with_capacity(num_rounds); - - let mut round_witness = ECPairingRoundWitness { - new_request: None, - reads: [MemoryQuery::empty(); MEMORY_READS_PER_CYCLE], - writes: None, - }; - - // Doing NUM_ROUNDS - for i in 0..num_rounds { - if i == 0 { - round_witness.new_request = Some(precompile_call_params); - } - // we assume that we have - // - x1 as U256 as a first coordinate of the first point (32 bytes) - // - y1 as U256 as a second coordinate of the first point (32 bytes) - // - x2 as U256 as a c0 component of first coordinate of the second point (32 bytes) - // - y2 as U256 as a c1 component of first coordinate of the second point (32 bytes) - // - x3 as U256 as a c0 component of second coordinate of the second point (32 bytes) - // - y3 as U256 as a c1 component of second coordinate of the second point (32 bytes) - - let x1_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let x1_query = memory.execute_partial_query(monotonic_cycle_counter, x1_query); - let x1_value = x1_query.value; - if B { - round_witness.reads[0] = x1_query; - read_history.push(x1_query); - } - - current_read_location.index.0 += 1; - let y1_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let y1_query = memory.execute_partial_query(monotonic_cycle_counter, y1_query); - let y1_value = y1_query.value; - if B { - round_witness.reads[1] = y1_query; - read_history.push(y1_query); - } - - current_read_location.index.0 += 1; - let x2_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let x2_query = memory.execute_partial_query(monotonic_cycle_counter, x2_query); - let x2_value = x2_query.value; - if B { - round_witness.reads[2] = x2_query; - read_history.push(x2_query); - } - - current_read_location.index.0 += 1; - let y2_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let y2_query = memory.execute_partial_query(monotonic_cycle_counter, y2_query); - let y2_value = y2_query.value; - if B { - round_witness.reads[3] = y2_query; - read_history.push(y2_query); - } - - current_read_location.index.0 += 1; - let x3_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let x3_query = memory.execute_partial_query(monotonic_cycle_counter, x3_query); - let x3_value = x3_query.value; - if B { - round_witness.reads[4] = x3_query; - read_history.push(x3_query); - } - - current_read_location.index.0 += 1; - let y3_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let y3_query = memory.execute_partial_query(monotonic_cycle_counter, y3_query); - let y3_value = y3_query.value; - if B { - round_witness.reads[5] = y3_query; - read_history.push(y3_query); - } - current_read_location.index.0 += 1; - - let last_round = i == num_rounds - 1; - // We'll add write queries into last round witness separately - if !last_round { - witnesses.push(round_witness.clone()); - } - // Setting check tuples - check_tuples.push([x1_value, y1_value, x2_value, y2_value, x3_value, y3_value]); - } - - #[allow(unused_assignments)] - let mut ok_or_err_query = MemoryQuery::empty(); - #[allow(unused_assignments)] - let mut result_query = MemoryQuery::empty(); - - // Performing ecpairing check - let pairing_check = ecpairing_inner(check_tuples.to_vec()); - - if let Ok(result) = pairing_check { - let mut write_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_write), - index: MemoryIndex(params.output_memory_offset), - }; - - // Marking that the operation was successful - let ok_marker = U256::one(); - ok_or_err_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: ok_marker, - value_is_pointer: false, - rw_flag: true, - }; - ok_or_err_query = - memory.execute_partial_query(monotonic_cycle_counter, ok_or_err_query); - - // Converting result to one if true and zero otherwise - let mut output_value = U256::zero(); - if result { - output_value = U256::one(); - } - - write_location.index.0 += 1; - result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: output_value, - value_is_pointer: false, - rw_flag: true, - }; - result_query = memory.execute_partial_query(monotonic_cycle_counter, result_query); - - if B { - write_history.push(ok_or_err_query); - write_history.push(result_query); - } - } else { - let mut write_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_write), - index: MemoryIndex(params.output_memory_offset), - }; - - let err_marker = U256::zero(); - ok_or_err_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: err_marker, - value_is_pointer: false, - rw_flag: true, - }; - ok_or_err_query = - memory.execute_partial_query(monotonic_cycle_counter, ok_or_err_query); - - write_location.index.0 += 1; - let empty_result = U256::zero(); - result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: empty_result, - value_is_pointer: false, - rw_flag: true, - }; - result_query = memory.execute_partial_query(monotonic_cycle_counter, result_query); - - if B { - write_history.push(ok_or_err_query); - write_history.push(result_query); - } - } - - let witness = if B { - round_witness.writes = Some([ok_or_err_query, result_query]); - witnesses.push(round_witness); - Some((read_history, write_history, witnesses)) - } else { - None - }; - - (num_rounds, witness) - } -} - -/// This function checks whether the pairing of two points on the elliptic curve -/// produces one. -/// -/// If the points are not on the curve or coordinates are not valid field elements, -/// the function will return an error. -pub fn ecpairing_inner(inputs: Vec) -> Result { - // If input is empty, return true according to EIP-197 - if inputs.len() == 0 { - return Ok(true); - } - - let mut total_pairing = Fq12::one(); - for input in inputs { - let pairing = pair(&input)?; - total_pairing.mul_assign(&pairing); - } - - Ok(total_pairing.eq(&Fq12::one())) -} - -/// Subgroup check for G2 using the Frobenius endomorphism. -/// Based on the property: ψ(P) == [6x^2]P for BN254. -fn check_if_in_subgroup(point: G2Affine) -> bool { - let mut x_p = point.into_projective(); - - let x = bn256::Fr::from_str("147946756881789318990833708069417712966").unwrap(); - x_p.mul_assign(x); - let (mut pi_1_q_x, mut pi_1_q_y) = point.into_xy_unchecked(); - - pi_1_q_x.conjugate(); - pi_1_q_x.mul_assign(&FROBENIUS_COEFF_FQ6_C1[1]); - pi_1_q_y.conjugate(); - pi_1_q_y.mul_assign(&XI_TO_Q_MINUS_1_OVER_2); - let frob_affine = G2Affine::from_xy_checked(pi_1_q_x, pi_1_q_y).unwrap(); - - x_p == frob_affine.into_projective() -} - -pub fn pair(input: &EcPairingInputTuple) -> Result { - // Setting variables for the coordinates of the points - let (x1, y1, x2, y2, x3, y3) = (input[0], input[1], input[2], input[3], input[4], input[5]); - - if !validate_values_in_field(&[ - &x1.to_string(), - &y1.to_string(), - &x2.to_string(), - &y2.to_string(), - &x3.to_string(), - &y3.to_string(), - ]) { - return Err(Error::msg("invalid values")); - } - - // Converting coordinates to the finite field format - // and validating that the conversion is successful - let x1_field = Fq::from_str(x1.to_string().as_str()).ok_or(Error::msg("invalid x1"))?; - let y1_field = Fq::from_str(y1.to_string().as_str()).ok_or(Error::msg("invalid y1"))?; - let x2_field = Fq::from_str(x2.to_string().as_str()).ok_or(Error::msg("invalid x2"))?; - let y2_field = Fq::from_str(y2.to_string().as_str()).ok_or(Error::msg("invalid y2"))?; - let x3_field = Fq::from_str(x3.to_string().as_str()).ok_or(Error::msg("invalid x3"))?; - let y3_field = Fq::from_str(y3.to_string().as_str()).ok_or(Error::msg("invalid y3"))?; - - // Setting both points. - // NOTE: If one of the points is zero, then both coordinates are zero, - // which aligns with the from_xy_checked method implementation. - let point_1 = G1Affine::from_xy_checked(x1_field, y1_field)?; - - // NOTE: In EIP-197 spec, 3rd and 5th positions correspond to imaginary part, while 4th and 6th to real ones. - // Thus, it might be confusing why we switch the order below. - let point_2_x = Fq2 { - c0: y2_field, - c1: x2_field, - }; - let point_2_y = Fq2 { - c0: y3_field, - c1: x3_field, - }; - let point_2 = G2Affine::from_xy_checked(point_2_x, point_2_y)?; - - if !check_if_in_subgroup(point_2) { - anyhow::bail!("G2 not on the subgroup"); - } - - // Calculating the pairing operation and returning - let pairing = point_1.pairing_with(&point_2); - Ok(pairing) -} - -pub fn ecpairing_function( - monotonic_cycle_counter: u32, - precompile_call_params: LogQuery, - memory: &mut M, -) -> ( - usize, - Option<( - Vec, - Vec, - Vec, - )>, -) { - let mut processor = ECPairingPrecompile::; - processor.execute_precompile(monotonic_cycle_counter, precompile_call_params, memory) -} - -#[cfg(test)] -pub mod tests { - /// Tests the correctness of the `ecpairing_inner` by providing two valid points on the curve - /// that do not produce one as an output. - /// Here, point G2 is in the wrong subgroup. - #[test] - fn test_ecpairing_inner_correctness_false() { - use super::*; - - let x1 = U256::from_str_radix( - "0x412aa5b0805215b55a5e2dbf0662031aad0f5ef13f28b25df20b8670d1c59a6", - 16, - ) - .unwrap(); - let y1 = U256::from_str_radix( - "0x16fb4b64ccff216fa5272e1e987c0616d60d8883d5834229c685949047e9411d", - 16, - ) - .unwrap(); - - let x2 = U256::from_str_radix( - "0x2d81dbc969f72bc0454ff8b04735b717b725fee98a2fcbcdcf6c5b51b1dff33f", - 16, - ) - .unwrap(); - let y2 = U256::from_str_radix( - "0x75239888fc8448ab781e2a8bb85eb556469474cd707d4b913bee28679920eb6", - 16, - ) - .unwrap(); - - let x3 = U256::from_str_radix( - "0x1ef1c268b7c4c78959f099a043ecd5e537fe3069ac9197235f16162372848cba", - 16, - ) - .unwrap(); - let y3 = U256::from_str_radix( - "0x209cfadc22f7e80d399d1886f1c53898521a34c62918ed802305f32b4070a3c4", - 16, - ) - .unwrap(); - - let result = ecpairing_inner(vec![[x1, y1, x2, y2, x3, y3]]); - assert!(result.is_err(), "Expected precompile to fail"); - - assert_eq!(&result.err().unwrap().to_string(), "G2 not on the subgroup"); - } - - /// Tests the correctness of the `ecpairing_inner` by providing four valid points on the curve - /// that do not produce one as an output. Example is taken from https://www.evm.codes/precompiled#0x08 - #[test] - fn test_ecpairing_inner_correctness_true() { - use super::*; - - let x1_1 = U256::from_str_radix( - "0x2cf44499d5d27bb186308b7af7af02ac5bc9eeb6a3d147c186b21fb1b76e18da", - 16, - ) - .unwrap(); - let y1_1 = U256::from_str_radix( - "0x2c0f001f52110ccfe69108924926e45f0b0c868df0e7bde1fe16d3242dc715f6", - 16, - ) - .unwrap(); - - let x2_1 = U256::from_str_radix( - "0x1fb19bb476f6b9e44e2a32234da8212f61cd63919354bc06aef31e3cfaff3ebc", - 16, - ) - .unwrap(); - let y2_1 = U256::from_str_radix( - "0x22606845ff186793914e03e21df544c34ffe2f2f3504de8a79d9159eca2d98d9", - 16, - ) - .unwrap(); - - let x3_1 = U256::from_str_radix( - "0x2bd368e28381e8eccb5fa81fc26cf3f048eea9abfdd85d7ed3ab3698d63e4f90", - 16, - ) - .unwrap(); - let y3_1 = U256::from_str_radix( - "0x2fe02e47887507adf0ff1743cbac6ba291e66f59be6bd763950bb16041a0a85e", - 16, - ) - .unwrap(); - - let x1_2 = U256::from_str_radix( - "0x0000000000000000000000000000000000000000000000000000000000000001", - 16, - ) - .unwrap(); - let y1_2 = U256::from_str_radix( - "0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45", - 16, - ) - .unwrap(); - - let x2_2 = U256::from_str_radix( - "0x1971ff0471b09fa93caaf13cbf443c1aede09cc4328f5a62aad45f40ec133eb4", - 16, - ) - .unwrap(); - let y2_2 = U256::from_str_radix( - "0x091058a3141822985733cbdddfed0fd8d6c104e9e9eff40bf5abfef9ab163bc7", - 16, - ) - .unwrap(); - - let x3_2 = U256::from_str_radix( - "0x2a23af9a5ce2ba2796c1f4e453a370eb0af8c212d9dc9acd8fc02c2e907baea2", - 16, - ) - .unwrap(); - let y3_2 = U256::from_str_radix( - "0x23a8eb0b0996252cb548a4487da97b02422ebc0e834613f954de6c7e0afdc1fc", - 16, - ) - .unwrap(); - - let result = ecpairing_inner(vec![ - [x1_1, y1_1, x2_1, y2_1, x3_1, y3_1], - [x1_2, y1_2, x2_2, y2_2, x3_2, y3_2], - ]) - .unwrap(); - - println!( - "{:?}", - vec![ - [x1_1, y1_1, x2_1, y2_1, x3_1, y3_1], - [x1_2, y1_2, x2_2, y2_2, x3_2, y3_2], - ] - ); - - assert_eq!(result, true); - } - - /// Tests the correctness of the `ecpairing_inner` by providing four valid points on the curve - /// and the rest are empty points. Example is taken from https://www.evm.codes/precompiled#0x08. - #[test] - fn test_ecpairing_inner_correctness_zero_inputs() { - use super::*; - - let x1_1 = U256::from_str_radix( - "0x2cf44499d5d27bb186308b7af7af02ac5bc9eeb6a3d147c186b21fb1b76e18da", - 16, - ) - .unwrap(); - let y1_1 = U256::from_str_radix( - "0x2c0f001f52110ccfe69108924926e45f0b0c868df0e7bde1fe16d3242dc715f6", - 16, - ) - .unwrap(); - - let x2_1 = U256::from_str_radix( - "0x1fb19bb476f6b9e44e2a32234da8212f61cd63919354bc06aef31e3cfaff3ebc", - 16, - ) - .unwrap(); - let y2_1 = U256::from_str_radix( - "0x22606845ff186793914e03e21df544c34ffe2f2f3504de8a79d9159eca2d98d9", - 16, - ) - .unwrap(); - - let x3_1 = U256::from_str_radix( - "0x2bd368e28381e8eccb5fa81fc26cf3f048eea9abfdd85d7ed3ab3698d63e4f90", - 16, - ) - .unwrap(); - let y3_1 = U256::from_str_radix( - "0x2fe02e47887507adf0ff1743cbac6ba291e66f59be6bd763950bb16041a0a85e", - 16, - ) - .unwrap(); - - let x1_2 = U256::from_str_radix( - "0x0000000000000000000000000000000000000000000000000000000000000001", - 16, - ) - .unwrap(); - let y1_2 = U256::from_str_radix( - "0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45", - 16, - ) - .unwrap(); - - let x2_2 = U256::from_str_radix( - "0x1971ff0471b09fa93caaf13cbf443c1aede09cc4328f5a62aad45f40ec133eb4", - 16, - ) - .unwrap(); - let y2_2 = U256::from_str_radix( - "0x091058a3141822985733cbdddfed0fd8d6c104e9e9eff40bf5abfef9ab163bc7", - 16, - ) - .unwrap(); - - let x3_2 = U256::from_str_radix( - "0x2a23af9a5ce2ba2796c1f4e453a370eb0af8c212d9dc9acd8fc02c2e907baea2", - 16, - ) - .unwrap(); - let y3_2 = U256::from_str_radix( - "0x23a8eb0b0996252cb548a4487da97b02422ebc0e834613f954de6c7e0afdc1fc", - 16, - ) - .unwrap(); - - let empty_input = [U256::zero(); 6]; - - let result = ecpairing_inner(vec![ - [x1_1, y1_1, x2_1, y2_1, x3_1, y3_1], - [x1_2, y1_2, x2_2, y2_2, x3_2, y3_2], - empty_input, - empty_input, - empty_input, - empty_input, - ]) - .unwrap(); - - assert_eq!(result, true); - } - - /// Tests that the function does not allow to input a wrong first point. - #[test] - #[should_panic] - fn test_ecpairing_invalid_point_1() { - use super::*; - - // (x1, y1) does not lie on the curve - let x1 = U256::from_str_radix("5", 10).unwrap(); - let y1 = U256::from_str_radix("10", 10).unwrap(); - - let x2 = U256::from_str_radix( - "0x16342ef5343ae56e96dafd3fc43aaf6a715642f376327cf2bdb813cf41a0b55b", - 16, - ) - .unwrap(); - let y2 = U256::from_str_radix( - "0x237e8c97323c9032ce9e05af4b1597881131d137b5313182c9ef1b2576c9f3f1", - 16, - ) - .unwrap(); - - let x3 = U256::from_str_radix( - "0x9c316c01492b5d4e2521d897b66de1e47438adf83a320054f8fc763935dc754", - 16, - ) - .unwrap(); - let y3 = U256::from_str_radix( - "0xe1bf45145e9ee5372a81f2ad50b81830e3bb26400a5a72999fac2f73d768089", - 16, - ) - .unwrap(); - - let _ = ecpairing_inner(vec![[x1, y1, x2, y2, x3, y3]]).unwrap(); - } - - /// Tests that the function does not allow to input a wrong second point. - #[test] - #[should_panic] - fn test_ecpairing_invalid_point_2() { - use super::*; - - let x1 = U256::from_str_radix( - "0x412aa5b0805215b55a5e2dbf0662031aad0f5ef13f28b25df20b8670d1c59a6", - 16, - ) - .unwrap(); - let y1 = U256::from_str_radix( - "0x16fb4b64ccff216fa5272e1e987c0616d60d8883d5834229c685949047e9411d", - 16, - ) - .unwrap(); - - // ((x2,y2), (x3,y3)) does not lie on the curve - let x2 = U256::from_str_radix("0", 10).unwrap(); - let y2 = U256::from_str_radix("1", 10).unwrap(); - - let x3 = U256::from_str_radix("2", 10).unwrap(); - let y3 = U256::from_str_radix("3", 10).unwrap(); - - let _ = ecpairing_inner(vec![[x1, y1, x2, y2, x3, y3]]).unwrap(); - } - #[test] - fn test_check_if_in_subgroup_infinity() { - use super::*; - - let infinity_point = G2Affine::zero(); - - // This should return true, because the group identity is always in the subgroup. - assert!( - check_if_in_subgroup(infinity_point), - "infinity point should be in the subgroup" - ); - } -} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecpairing/airbender_backend.rs b/crates/zk_evm_abstractions/src/precompiles/ecpairing/airbender_backend.rs new file mode 100644 index 00000000..1d04dd86 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecpairing/airbender_backend.rs @@ -0,0 +1,88 @@ +use airbender_crypto::ark_ec::pairing::Pairing as AirPairing; +use airbender_crypto::ark_ec::AffineRepr as AirAffineRepr; +use airbender_crypto::ark_ff::{One as AirOne, Zero as AirZero}; +use airbender_crypto::bn254::{ + curves::Bn254 as AirBn254, Fq12 as AirFq12, Fq2 as AirFq2, G1Affine as AirG1Affine, + G2Affine as AirG2Affine, +}; +use anyhow::{Error, Result}; + +use crate::utils::airbender_bn254::airbender_fq_from_u256; +use crate::utils::bn254::validate_values_in_field; + +use super::{ECPairingBackend, EcPairingInputTuple}; + +// ============================================================================== +// Delegated Backend +// ============================================================================== +pub(super) struct DelegatedECPairingBackend; + +impl ECPairingBackend for DelegatedECPairingBackend { + fn pairing(inputs: Vec) -> Result { + if inputs.is_empty() { + return Ok(true); + } + + let mut total_pairing = AirFq12::one(); + for input in inputs { + let pairing = pair_airbender(&input)?; + total_pairing *= &pairing; + } + + Ok(total_pairing.eq(&AirFq12::one())) + } +} + +fn pair_airbender(input: &EcPairingInputTuple) -> Result { + let (x1, y1, x2, y2, x3, y3) = (input[0], input[1], input[2], input[3], input[4], input[5]); + + if !validate_values_in_field(&[ + &x1.to_string(), + &y1.to_string(), + &x2.to_string(), + &y2.to_string(), + &x3.to_string(), + &y3.to_string(), + ]) { + return Err(Error::msg("invalid values")); + } + + let x1_field = airbender_fq_from_u256(x1, "invalid x1")?; + let y1_field = airbender_fq_from_u256(y1, "invalid y1")?; + let x2_field = airbender_fq_from_u256(x2, "invalid x2")?; + let y2_field = airbender_fq_from_u256(y2, "invalid y2")?; + let x3_field = airbender_fq_from_u256(x3, "invalid x3")?; + let y3_field = airbender_fq_from_u256(y3, "invalid y3")?; + + let point_1 = if x1.is_zero() && y1.is_zero() { + AirG1Affine::zero() + } else { + let point = AirG1Affine::new_unchecked(x1_field, y1_field); + if !point.is_on_curve() { + return Err(Error::msg("G1 point is not on curve")); + } + if !point.is_in_correct_subgroup_assuming_on_curve() { + return Err(Error::msg("G1 point is not in subgroup")); + } + point + }; + + // NOTE: In EIP-197, the tuple stores the imaginary component before the real one. + let point_2_x = AirFq2::new(y2_field, x2_field); + let point_2_y = AirFq2::new(y3_field, x3_field); + + let point_2 = if point_2_x.is_zero() && point_2_y.is_zero() { + AirG2Affine::zero() + } else { + let point = AirG2Affine::new_unchecked(point_2_x, point_2_y); + if !point.is_on_curve() { + return Err(Error::msg("G2 point is not on curve")); + } + if !point.is_in_correct_subgroup_assuming_on_curve() { + anyhow::bail!("G2 not on the subgroup"); + } + point + }; + + Ok(::pairing(point_1, point_2).0) +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecpairing/legacy_backend.rs b/crates/zk_evm_abstractions/src/precompiles/ecpairing/legacy_backend.rs new file mode 100644 index 00000000..ef6362d0 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecpairing/legacy_backend.rs @@ -0,0 +1,26 @@ +use anyhow::Result; +use zkevm_opcode_defs::bn254::bn256::Fq12; +use zkevm_opcode_defs::bn254::ff::Field; + +use super::{pair, ECPairingBackend, EcPairingInputTuple}; + +// ============================================================================== +// Legacy Backend +// ============================================================================== +pub(super) struct LegacyECPairingBackend; + +impl ECPairingBackend for LegacyECPairingBackend { + fn pairing(inputs: Vec) -> Result { + if inputs.is_empty() { + return Ok(true); + } + + let mut total_pairing = Fq12::one(); + for input in inputs { + let pairing = pair(&input)?; + total_pairing.mul_assign(&pairing); + } + + Ok(total_pairing.eq(&Fq12::one())) + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecpairing/mod.rs b/crates/zk_evm_abstractions/src/precompiles/ecpairing/mod.rs new file mode 100644 index 00000000..c344c594 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecpairing/mod.rs @@ -0,0 +1,275 @@ +use anyhow::{Error, Result}; +use cfg_if::cfg_if; +use zkevm_opcode_defs::bn254::bn256::{ + self, Fq, Fq12, Fq2, G1Affine, G2Affine, FROBENIUS_COEFF_FQ6_C1, XI_TO_Q_MINUS_1_OVER_2, +}; +use zkevm_opcode_defs::bn254::ff::{Field, PrimeField}; +use zkevm_opcode_defs::bn254::{CurveAffine, CurveProjective}; +use zkevm_opcode_defs::ethereum_types::U256; + +use super::*; +use crate::utils::bn254::validate_values_in_field; + +#[cfg(feature = "airbender-precompile-delegations")] +mod airbender_backend; +#[cfg(any(not(feature = "airbender-precompile-delegations"), test))] +mod legacy_backend; +#[cfg(test)] +mod tests; + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use self::airbender_backend::DelegatedECPairingBackend as ActiveECPairingBackend; + } else { + use self::legacy_backend::LegacyECPairingBackend as ActiveECPairingBackend; + } +} + +// NOTE: We need x1, y1, x2, y2, x3, y3. +pub const MEMORY_READS_PER_CYCLE: usize = 6; +// NOTE: We write the status marker plus the pairing result. +pub const MEMORY_WRITES_PER_CYCLE: usize = 2; + +type EcPairingInputTuple = [U256; 6]; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ECPairingRoundWitness { + pub new_request: Option, + pub reads: [MemoryQuery; MEMORY_READS_PER_CYCLE], + pub writes: Option<[MemoryQuery; MEMORY_WRITES_PER_CYCLE]>, +} + +trait ECPairingBackend { + fn pairing(inputs: Vec) -> Result; +} + +// ============================================================================== +// Legacy-Compatible Pairing Helpers +// ============================================================================== +// +// `zkevm_test_harness` reconstructs the precompile's internal pairing accumulator +// while building witnesses, so the legacy `pair` helper remains part of the module's +// public surface even after the backend split. + +pub(super) fn check_if_in_subgroup(point: G2Affine) -> bool { + let mut x_p = point.into_projective(); + + let x = bn256::Fr::from_str("147946756881789318990833708069417712966").unwrap(); + x_p.mul_assign(x); + let (mut pi_1_q_x, mut pi_1_q_y) = point.into_xy_unchecked(); + + pi_1_q_x.conjugate(); + pi_1_q_x.mul_assign(&FROBENIUS_COEFF_FQ6_C1[1]); + pi_1_q_y.conjugate(); + pi_1_q_y.mul_assign(&XI_TO_Q_MINUS_1_OVER_2); + let frob_affine = G2Affine::from_xy_checked(pi_1_q_x, pi_1_q_y).unwrap(); + + x_p == frob_affine.into_projective() +} + +pub fn pair(input: &EcPairingInputTuple) -> Result { + let (x1, y1, x2, y2, x3, y3) = (input[0], input[1], input[2], input[3], input[4], input[5]); + + if !validate_values_in_field(&[ + &x1.to_string(), + &y1.to_string(), + &x2.to_string(), + &y2.to_string(), + &x3.to_string(), + &y3.to_string(), + ]) { + return Err(Error::msg("invalid values")); + } + + let x1_field = Fq::from_str(x1.to_string().as_str()).ok_or(Error::msg("invalid x1"))?; + let y1_field = Fq::from_str(y1.to_string().as_str()).ok_or(Error::msg("invalid y1"))?; + let x2_field = Fq::from_str(x2.to_string().as_str()).ok_or(Error::msg("invalid x2"))?; + let y2_field = Fq::from_str(y2.to_string().as_str()).ok_or(Error::msg("invalid y2"))?; + let x3_field = Fq::from_str(x3.to_string().as_str()).ok_or(Error::msg("invalid x3"))?; + let y3_field = Fq::from_str(y3.to_string().as_str()).ok_or(Error::msg("invalid y3"))?; + + let point_1 = G1Affine::from_xy_checked(x1_field, y1_field)?; + + // NOTE: In EIP-197, the tuple stores the imaginary component before the real one. + let point_2_x = Fq2 { + c0: y2_field, + c1: x2_field, + }; + let point_2_y = Fq2 { + c0: y3_field, + c1: x3_field, + }; + let point_2 = G2Affine::from_xy_checked(point_2_x, point_2_y)?; + + if !check_if_in_subgroup(point_2) { + anyhow::bail!("G2 not on the subgroup"); + } + + Ok(point_1.pairing_with(&point_2)) +} + +fn execute_ecpairing_precompile( + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<( + Vec, + Vec, + Vec, + )>, +) { + let precompile_call_params = query; + let params = precompile_abi_in_log(precompile_call_params); + let num_rounds = params.precompile_interpreted_data as usize; + let timestamp_to_read = precompile_call_params.timestamp; + let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); + + let mut current_read_location = MemoryLocation { + memory_type: MemoryType::Heap, + page: MemoryPage(params.memory_page_to_read), + index: MemoryIndex(params.input_memory_offset), + }; + + let mut read_history = if B { + Vec::with_capacity(num_rounds * MEMORY_READS_PER_CYCLE) + } else { + vec![] + }; + let mut write_history = if B { + Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) + } else { + vec![] + }; + + let mut inputs = Vec::with_capacity(num_rounds); + let mut witnesses = if B { + Vec::with_capacity(num_rounds) + } else { + vec![] + }; + + for round_idx in 0..num_rounds { + let mut round_witness = ECPairingRoundWitness { + new_request: (round_idx == 0).then_some(precompile_call_params), + reads: [MemoryQuery::empty(); MEMORY_READS_PER_CYCLE], + writes: None, + }; + + let mut input_tuple = [U256::zero(); MEMORY_READS_PER_CYCLE]; + for (idx, input_word) in input_tuple.iter_mut().enumerate() { + let read_query = MemoryQuery { + timestamp: timestamp_to_read, + location: current_read_location, + value: U256::zero(), + value_is_pointer: false, + rw_flag: false, + }; + let read_query = memory.execute_partial_query(monotonic_cycle_counter, read_query); + *input_word = read_query.value; + + if B { + round_witness.reads[idx] = read_query; + read_history.push(read_query); + } + + current_read_location.index.0 += 1; + } + + inputs.push(input_tuple); + if B { + witnesses.push(round_witness); + } + } + + let result = Backend::pairing(inputs); + let output_values = match result { + Ok(is_valid) => [U256::one(), U256::from(is_valid as u64)], + Err(_) => [U256::zero(), U256::zero()], + }; + + let mut write_location = MemoryLocation { + memory_type: MemoryType::Heap, + page: MemoryPage(params.memory_page_to_write), + index: MemoryIndex(params.output_memory_offset), + }; + + let mut write_queries = [MemoryQuery::empty(); MEMORY_WRITES_PER_CYCLE]; + for (idx, value) in output_values.into_iter().enumerate() { + let write_query = MemoryQuery { + timestamp: timestamp_to_write, + location: write_location, + value, + value_is_pointer: false, + rw_flag: true, + }; + let write_query = memory.execute_partial_query(monotonic_cycle_counter, write_query); + write_queries[idx] = write_query; + + if B { + write_history.push(write_query); + } + + write_location.index.0 += 1; + } + + let witness = if B { + if let Some(last_round) = witnesses.last_mut() { + last_round.writes = Some(write_queries); + } else { + witnesses.push(ECPairingRoundWitness { + new_request: None, + reads: [MemoryQuery::empty(); MEMORY_READS_PER_CYCLE], + writes: Some(write_queries), + }); + } + Some((read_history, write_history, witnesses)) + } else { + None + }; + + (num_rounds, witness) +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ECPairingPrecompile; + +impl Precompile for ECPairingPrecompile { + type CycleWitness = ECPairingRoundWitness; + + fn execute_precompile( + &mut self, + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, + ) -> ( + usize, + Option<(Vec, Vec, Vec)>, + ) { + execute_ecpairing_precompile::( + monotonic_cycle_counter, + query, + memory, + ) + } +} + +pub fn ecpairing_function( + monotonic_cycle_counter: u32, + precompile_call_params: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<( + Vec, + Vec, + Vec, + )>, +) { + execute_ecpairing_precompile::( + monotonic_cycle_counter, + precompile_call_params, + memory, + ) +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecpairing/tests/mod.rs b/crates/zk_evm_abstractions/src/precompiles/ecpairing/tests/mod.rs new file mode 100644 index 00000000..b90ba802 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecpairing/tests/mod.rs @@ -0,0 +1,97 @@ +use cfg_if::cfg_if; +use quickcheck::QuickCheck; + +use super::{check_if_in_subgroup, ecpairing_function, legacy_backend::LegacyECPairingBackend}; + +mod utils; + +use self::utils::{ + assert_backend_matches_case, deterministic_ecpairing_cases, empty_input_query, + legacy_backend_matches_inverse_pairing_case, point_at_infinity_ecpairing_cases, + TestPrecompileMemory, QUICKCHECK_NUM_CASES, +}; + +#[test] +fn legacy_backend_matches_static_vectors() { + for case in deterministic_ecpairing_cases() { + assert_backend_matches_case::(&case); + } +} + +#[test] +fn legacy_subgroup_check_accepts_infinity() { + use zkevm_opcode_defs::bn254::bn256::G2Affine; + use zkevm_opcode_defs::bn254::CurveAffine; + + assert!(check_if_in_subgroup(G2Affine::zero())); +} + +#[test] +fn legacy_backend_matches_point_at_infinity_vectors() { + for case in point_at_infinity_ecpairing_cases() { + assert_backend_matches_case::(&case); + } +} + +#[test] +fn empty_input_still_emits_a_final_witness_row() { + let mut memory = TestPrecompileMemory::default(); + let (num_rounds, witness) = + ecpairing_function::(0, empty_input_query(), &mut memory); + + assert_eq!(num_rounds, 0); + + let (_, write_history, rounds) = witness.expect("witnessed execution must produce a witness"); + assert_eq!(write_history.len(), 2); + assert_eq!(rounds.len(), 1); + assert!(rounds[0].new_request.is_none()); + assert!(rounds[0].writes.is_some()); + assert_eq!( + rounds[0].writes.unwrap(), + [write_history[0], write_history[1]] + ); +} + +#[test] +fn legacy_backend_matches_inverse_pairing_quickcheck() { + fn property(g1_scalar: u64, g2_scalar: u64) -> bool { + legacy_backend_matches_inverse_pairing_case::(g1_scalar, g2_scalar) + } + + QuickCheck::new() + .tests(QUICKCHECK_NUM_CASES) + .quickcheck(property as fn(u64, u64) -> bool); +} + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use super::airbender_backend::DelegatedECPairingBackend; + use self::utils::delegated_backend_matches_inverse_pairing_case; + + #[test] + fn delegated_backend_matches_static_vectors() { + for case in deterministic_ecpairing_cases() { + assert_backend_matches_case::(&case); + } + } + + #[test] + fn delegated_backend_matches_point_at_infinity_vectors() { + for case in point_at_infinity_ecpairing_cases() { + assert_backend_matches_case::(&case); + } + } + + #[test] + fn delegated_backend_matches_legacy_quickcheck() { + fn property(g1_scalar: u64, g2_scalar: u64) -> bool { + legacy_backend_matches_inverse_pairing_case::(g1_scalar, g2_scalar) + && delegated_backend_matches_inverse_pairing_case::(g1_scalar, g2_scalar) + } + + QuickCheck::new() + .tests(QUICKCHECK_NUM_CASES) + .quickcheck(property as fn(u64, u64) -> bool); + } + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecpairing/tests/utils.rs b/crates/zk_evm_abstractions/src/precompiles/ecpairing/tests/utils.rs new file mode 100644 index 00000000..ccad2b5c --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecpairing/tests/utils.rs @@ -0,0 +1,295 @@ +use cfg_if::cfg_if; +use std::str::FromStr; + +use zkevm_opcode_defs::bn254::bn256::{Fq, Fr, G1Affine, G2Affine}; +use zkevm_opcode_defs::bn254::ff::PrimeField; +use zkevm_opcode_defs::bn254::{CurveAffine, CurveProjective}; +use zkevm_opcode_defs::ethereum_types::{Address, U256}; +use zkevm_opcode_defs::PrecompileCallABI; + +use super::super::{ECPairingBackend, EcPairingInputTuple}; +use crate::aux::Timestamp; +use crate::queries::{LogQuery, MemoryQuery}; +use crate::vm::Memory; + +pub(super) const QUICKCHECK_NUM_CASES: u64 = 256; + +pub(super) enum PairingExpectation { + Ok(bool), + AnyErr, + ErrMessage(&'static str), +} + +pub(super) struct DeterministicECPairingCase { + name: &'static str, + inputs: Vec, + expected: PairingExpectation, +} + +pub(super) fn deterministic_ecpairing_cases() -> Vec { + let mut cases = vec![ + DeterministicECPairingCase { + name: "empty-input", + inputs: vec![], + expected: PairingExpectation::Ok(true), + }, + DeterministicECPairingCase { + name: "evm-codes-true-case", + inputs: valid_true_case(), + expected: PairingExpectation::Ok(true), + }, + DeterministicECPairingCase { + name: "valid-generator-pairing-false-case", + inputs: vec![pairing_tuple(G1Affine::one(), G2Affine::one())], + expected: PairingExpectation::Ok(false), + }, + DeterministicECPairingCase { + name: "evm-codes-true-case-with-zeros", + inputs: { + let mut inputs = valid_true_case(); + inputs.extend([[U256::zero(); 6]; 4]); + inputs + }, + expected: PairingExpectation::Ok(true), + }, + DeterministicECPairingCase { + name: "subgroup-invalid-case", + inputs: subgroup_invalid_case(), + expected: PairingExpectation::ErrMessage("G2 not on the subgroup"), + }, + DeterministicECPairingCase { + name: "invalid-first-point", + inputs: invalid_point_case(), + expected: PairingExpectation::AnyErr, + }, + ]; + cases.extend(point_at_infinity_ecpairing_cases()); + cases +} + +pub(super) fn point_at_infinity_ecpairing_cases() -> Vec { + vec![ + DeterministicECPairingCase { + name: "g1-infinity-with-g2-generator", + inputs: vec![pairing_tuple(G1Affine::zero(), G2Affine::one())], + expected: PairingExpectation::Ok(true), + }, + DeterministicECPairingCase { + name: "g2-infinity-with-g1-generator", + inputs: vec![pairing_tuple(G1Affine::one(), G2Affine::zero())], + expected: PairingExpectation::Ok(true), + }, + DeterministicECPairingCase { + name: "both-points-infinity", + inputs: vec![pairing_tuple(G1Affine::zero(), G2Affine::zero())], + expected: PairingExpectation::Ok(true), + }, + ] +} + +pub(super) fn assert_backend_matches_case( + case: &DeterministicECPairingCase, +) { + match (&case.expected, Backend::pairing(case.inputs.clone())) { + (PairingExpectation::Ok(expected), Ok(actual)) => assert_eq!( + actual, *expected, + "backend must match static vector '{}'", + case.name, + ), + (PairingExpectation::AnyErr, Err(_)) => {} + (PairingExpectation::ErrMessage(expected), Err(actual)) => assert_eq!( + actual.to_string(), + *expected, + "backend must match static vector '{}'", + case.name, + ), + (PairingExpectation::Ok(_), Err(error)) => panic!( + "backend unexpectedly failed for vector '{}': {error}", + case.name, + ), + (PairingExpectation::AnyErr, Ok(actual)) => panic!( + "backend unexpectedly succeeded for vector '{}': {actual}", + case.name, + ), + (PairingExpectation::ErrMessage(expected), Ok(actual)) => panic!( + "backend unexpectedly succeeded for vector '{}' expected error '{}', got {actual}", + case.name, expected, + ), + } +} + +pub(super) fn legacy_backend_matches_inverse_pairing_case( + g1_scalar: u64, + g2_scalar: u64, +) -> bool { + backend_matches_expected::(inverse_pairing_case(g1_scalar, g2_scalar), true) +} + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { +pub(super) fn delegated_backend_matches_inverse_pairing_case( + g1_scalar: u64, + g2_scalar: u64, + ) -> bool { + backend_matches_expected::(inverse_pairing_case(g1_scalar, g2_scalar), true) + } + } +} + +#[derive(Debug, Default)] +pub(super) struct TestPrecompileMemory; + +impl Memory for TestPrecompileMemory { + fn execute_partial_query( + &mut self, + _monotonic_cycle_counter: u32, + query: MemoryQuery, + ) -> MemoryQuery { + query + } + + fn specialized_code_query( + &mut self, + _monotonic_cycle_counter: u32, + _query: MemoryQuery, + ) -> MemoryQuery { + unreachable!("ecpairing precompile does not issue code queries") + } + + fn read_code_query(&self, _monotonic_cycle_counter: u32, _query: MemoryQuery) -> MemoryQuery { + unreachable!("ecpairing precompile does not issue code queries") + } +} + +pub(super) fn empty_input_query() -> LogQuery { + let abi = PrecompileCallABI { + input_memory_offset: 0, + input_memory_length: 0, + output_memory_offset: 0, + output_memory_length: 2, + memory_page_to_read: 1, + memory_page_to_write: 2, + precompile_interpreted_data: 0, + }; + + LogQuery { + timestamp: Timestamp(1), + tx_number_in_block: 0, + aux_byte: 0, + shard_id: 0, + address: Address::zero(), + key: abi.to_u256(), + read_value: U256::zero(), + written_value: U256::zero(), + rw_flag: false, + rollback: false, + is_service: false, + } +} + +fn backend_matches_expected( + inputs: Vec, + expected: bool, +) -> bool { + matches!(Backend::pairing(inputs), Ok(actual) if actual == expected) +} + +fn inverse_pairing_case(g1_scalar: u64, g2_scalar: u64) -> Vec { + let g1_scalar = scalar_from_u64(g1_scalar.saturating_add(1)); + let g2_scalar = scalar_from_u64(g2_scalar.saturating_add(1)); + + let point_1 = G1Affine::one().mul(g1_scalar).into_affine(); + let point_2 = G2Affine::one().mul(g2_scalar).into_affine(); + let mut negative_point_1 = point_1.into_projective(); + negative_point_1.negate(); + + vec![ + pairing_tuple(point_1, point_2), + pairing_tuple(negative_point_1.into_affine(), point_2), + ] +} + +fn pairing_tuple(point_1: G1Affine, point_2: G2Affine) -> EcPairingInputTuple { + let (x1, y1) = g1_to_tuple(point_1); + let [x2, y2, x3, y3] = g2_to_tuple(point_2); + [x1, y1, x2, y2, x3, y3] +} + +fn g1_to_tuple(point: G1Affine) -> (U256, U256) { + if point.is_zero() { + return (U256::zero(), U256::zero()); + } + + let (x, y) = point.into_xy_unchecked(); + (fq_to_u256(x), fq_to_u256(y)) +} + +fn g2_to_tuple(point: G2Affine) -> [U256; 4] { + if point.is_zero() { + return [U256::zero(); 4]; + } + + let (x, y) = point.into_xy_unchecked(); + [ + fq_to_u256(x.c1), + fq_to_u256(x.c0), + fq_to_u256(y.c1), + fq_to_u256(y.c0), + ] +} + +fn fq_to_u256(value: Fq) -> U256 { + U256::from_str(format!("{}", value.into_repr()).as_str()) + .expect("BN254 field element must format as a decimal U256") +} + +fn scalar_from_u64(value: u64) -> Fr { + Fr::from_str(value.to_string().as_str()).expect("u64 scalar must fit into BN254 scalar field") +} + +fn subgroup_invalid_case() -> Vec { + vec![[ + u256_from_hex("0412aa5b0805215b55a5e2dbf0662031aad0f5ef13f28b25df20b8670d1c59a6"), + u256_from_hex("16fb4b64ccff216fa5272e1e987c0616d60d8883d5834229c685949047e9411d"), + u256_from_hex("2d81dbc969f72bc0454ff8b04735b717b725fee98a2fcbcdcf6c5b51b1dff33f"), + u256_from_hex("075239888fc8448ab781e2a8bb85eb556469474cd707d4b913bee28679920eb6"), + u256_from_hex("1ef1c268b7c4c78959f099a043ecd5e537fe3069ac9197235f16162372848cba"), + u256_from_hex("209cfadc22f7e80d399d1886f1c53898521a34c62918ed802305f32b4070a3c4"), + ]] +} + +fn valid_true_case() -> Vec { + vec![ + [ + u256_from_hex("2cf44499d5d27bb186308b7af7af02ac5bc9eeb6a3d147c186b21fb1b76e18da"), + u256_from_hex("2c0f001f52110ccfe69108924926e45f0b0c868df0e7bde1fe16d3242dc715f6"), + u256_from_hex("1fb19bb476f6b9e44e2a32234da8212f61cd63919354bc06aef31e3cfaff3ebc"), + u256_from_hex("22606845ff186793914e03e21df544c34ffe2f2f3504de8a79d9159eca2d98d9"), + u256_from_hex("2bd368e28381e8eccb5fa81fc26cf3f048eea9abfdd85d7ed3ab3698d63e4f90"), + u256_from_hex("2fe02e47887507adf0ff1743cbac6ba291e66f59be6bd763950bb16041a0a85e"), + ], + [ + u256_from_hex("0000000000000000000000000000000000000000000000000000000000000001"), + u256_from_hex("30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45"), + u256_from_hex("1971ff0471b09fa93caaf13cbf443c1aede09cc4328f5a62aad45f40ec133eb4"), + u256_from_hex("091058a3141822985733cbdddfed0fd8d6c104e9e9eff40bf5abfef9ab163bc7"), + u256_from_hex("2a23af9a5ce2ba2796c1f4e453a370eb0af8c212d9dc9acd8fc02c2e907baea2"), + u256_from_hex("23a8eb0b0996252cb548a4487da97b02422ebc0e834613f954de6c7e0afdc1fc"), + ], + ] +} + +fn invalid_point_case() -> Vec { + vec![[ + U256::from(5u64), + U256::from(10u64), + u256_from_hex("16342ef5343ae56e96dafd3fc43aaf6a715642f376327cf2bdb813cf41a0b55b"), + u256_from_hex("237e8c97323c9032ce9e05af4b1597881131d137b5313182c9ef1b2576c9f3f1"), + u256_from_hex("09c316c01492b5d4e2521d897b66de1e47438adf83a320054f8fc763935dc754"), + u256_from_hex("0e1bf45145e9ee5372a81f2ad50b81830e3bb26400a5a72999fac2f73d768089"), + ]] +} + +fn u256_from_hex(hex: &str) -> U256 { + U256::from_str_radix(hex, 16).expect("hex vector must parse as U256") +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecrecover.rs b/crates/zk_evm_abstractions/src/precompiles/ecrecover.rs deleted file mode 100644 index 91c973a3..00000000 --- a/crates/zk_evm_abstractions/src/precompiles/ecrecover.rs +++ /dev/null @@ -1,344 +0,0 @@ -use zkevm_opcode_defs::k256::ecdsa::VerifyingKey; -pub use zkevm_opcode_defs::sha2::Digest; -use zkevm_opcode_defs::{ethereum_types::U256, k256, sha3}; - -use super::*; - -// we need hash, r, s, v -pub const MEMORY_READS_PER_CYCLE: usize = 4; -pub const MEMORY_WRITES_PER_CYCLE: usize = 2; - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ECRecoverRoundWitness { - pub new_request: LogQuery, - pub reads: [MemoryQuery; MEMORY_READS_PER_CYCLE], - pub writes: [MemoryQuery; MEMORY_WRITES_PER_CYCLE], -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ECRecoverPrecompile; - -impl Precompile for ECRecoverPrecompile { - type CycleWitness = ECRecoverRoundWitness; - - fn execute_precompile( - &mut self, - monotonic_cycle_counter: u32, - query: LogQuery, - memory: &mut M, - ) -> ( - usize, - Option<(Vec, Vec, Vec)>, - ) { - const NUM_ROUNDS: usize = 1; - - // read the parameters - let precompile_call_params = query; - let params = precompile_abi_in_log(precompile_call_params); - let timestamp_to_read = precompile_call_params.timestamp; - let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); // our default timestamping agreement - - let mut current_read_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_read), - index: MemoryIndex(params.input_memory_offset), - }; - - // we assume that we have - // - hash of the message - // - r - // - s - // - v as a single byte - - // we do 6 queries per precompile - let mut read_history = if B { - Vec::with_capacity(MEMORY_READS_PER_CYCLE) - } else { - vec![] - }; - let mut write_history = if B { - Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) - } else { - vec![] - }; - - let mut round_witness = ECRecoverRoundWitness { - new_request: precompile_call_params, - reads: [MemoryQuery::empty(); MEMORY_READS_PER_CYCLE], - writes: [MemoryQuery::empty(); MEMORY_WRITES_PER_CYCLE], - }; - - let mut read_idx = 0; - - let hash_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let hash_query = memory.execute_partial_query(monotonic_cycle_counter, hash_query); - let hash_value = hash_query.value; - if B { - round_witness.reads[read_idx] = hash_query; - read_idx += 1; - read_history.push(hash_query); - } - - current_read_location.index.0 += 1; - let v_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let v_query = memory.execute_partial_query(monotonic_cycle_counter, v_query); - let v_value = v_query.value; - if B { - round_witness.reads[read_idx] = v_query; - read_idx += 1; - read_history.push(v_query); - } - - current_read_location.index.0 += 1; - let r_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let r_query = memory.execute_partial_query(monotonic_cycle_counter, r_query); - let r_value = r_query.value; - if B { - round_witness.reads[read_idx] = r_query; - read_idx += 1; - read_history.push(r_query); - } - - current_read_location.index.0 += 1; - let s_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let s_query = memory.execute_partial_query(monotonic_cycle_counter, s_query); - let s_value = s_query.value; - if B { - round_witness.reads[read_idx] = s_query; - read_history.push(s_query); - } - // read everything as bytes for ecrecover purposes - - let mut buffer = [0u8; 32]; - hash_value.to_big_endian(&mut buffer[..]); - let hash = buffer; - - r_value.to_big_endian(&mut buffer[..]); - let r_bytes = buffer; - - s_value.to_big_endian(&mut buffer[..]); - let s_bytes = buffer; - - v_value.to_big_endian(&mut buffer[..]); - let v = buffer[31]; - assert!(v == 0 || v == 1); - - let pk = ecrecover_inner(&hash, &r_bytes, &s_bytes, v); - - // here it may be possible to have non-recoverable k*G point, so can fail - if let Ok(recovered_pubkey) = pk { - let pk = k256::PublicKey::from(&recovered_pubkey); - let affine_point = pk.as_affine().clone(); - use k256::elliptic_curve::sec1::ToEncodedPoint; - let pk_bytes = affine_point.to_encoded_point(false); - let pk_bytes_ref: &[u8] = pk_bytes.as_ref(); - assert_eq!(pk_bytes_ref.len(), 65); - debug_assert_eq!(pk_bytes_ref[0], 0x04); - let address_hash = sha3::Keccak256::digest(&pk_bytes_ref[1..]); - - let mut address = [0u8; 32]; - let hash_ref: &[u8] = address_hash.as_ref(); - address[12..].copy_from_slice(&hash_ref[12..]); - - let mut write_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_write), - index: MemoryIndex(params.output_memory_offset), - }; - - let ok_marker = U256::one(); - let ok_or_err_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: ok_marker, - value_is_pointer: false, - rw_flag: true, - }; - let ok_or_err_query = - memory.execute_partial_query(monotonic_cycle_counter, ok_or_err_query); - - write_location.index.0 += 1; - let result = U256::from_big_endian(&address); - let result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: result, - value_is_pointer: false, - rw_flag: true, - }; - let result_query = memory.execute_partial_query(monotonic_cycle_counter, result_query); - - if B { - round_witness.writes[0] = ok_or_err_query; - round_witness.writes[1] = result_query; - write_history.push(ok_or_err_query); - write_history.push(result_query); - } - } else { - let mut write_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_write), - index: MemoryIndex(params.output_memory_offset), - }; - - let err_marker = U256::zero(); - let ok_or_err_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: err_marker, - value_is_pointer: false, - rw_flag: true, - }; - let ok_or_err_query = - memory.execute_partial_query(monotonic_cycle_counter, ok_or_err_query); - - write_location.index.0 += 1; - let empty_result = U256::zero(); - let result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: empty_result, - value_is_pointer: false, - rw_flag: true, - }; - let result_query = memory.execute_partial_query(monotonic_cycle_counter, result_query); - - if B { - round_witness.writes[0] = ok_or_err_query; - round_witness.writes[1] = result_query; - write_history.push(ok_or_err_query); - write_history.push(result_query); - } - } - - let witness = if B { - Some((read_history, write_history, vec![round_witness])) - } else { - None - }; - - (NUM_ROUNDS, witness) - } -} - -pub fn ecrecover_inner( - digest: &[u8; 32], - r: &[u8; 32], - s: &[u8; 32], - rec_id: u8, -) -> Result { - use k256::ecdsa::{RecoveryId, Signature}; - // r, s - let mut signature = [0u8; 64]; - signature[..32].copy_from_slice(r); - signature[32..].copy_from_slice(s); - // we expect pre-validation, so this check always works - let signature = Signature::try_from(&signature[..]).map_err(|_| ())?; - let recid = RecoveryId::try_from(rec_id).unwrap(); - - recover_no_malleability_check(digest, signature, recid) -} - -fn recover_no_malleability_check( - digest: &[u8; 32], - signature: k256::ecdsa::Signature, - recovery_id: k256::ecdsa::RecoveryId, -) -> Result { - use k256::ecdsa::hazmat::bits2field; - use k256::elliptic_curve::bigint::CheckedAdd; - use k256::elliptic_curve::generic_array::GenericArray; - use k256::elliptic_curve::ops::Invert; - use k256::elliptic_curve::ops::LinearCombination; - use k256::elliptic_curve::ops::Reduce; - use k256::elliptic_curve::point::DecompressPoint; - use k256::elliptic_curve::Curve; - use k256::elliptic_curve::FieldBytesEncoding; - use k256::elliptic_curve::PrimeField; - use k256::AffinePoint; - use k256::ProjectivePoint; - use k256::Scalar; - - let (r, s) = signature.split_scalars(); - let z = >::reduce_bytes( - &bits2field::(digest).map_err(|_| ())?, - ); - - let mut r_bytes: GenericArray::FieldBytesSize> = r.to_repr(); - if recovery_id.is_x_reduced() { - match Option::::from( - >::decode_field_bytes(&r_bytes) - .checked_add(&k256::Secp256k1::ORDER), - ) { - Some(restored) => { - r_bytes = >::encode_field_bytes( - &restored, - ) - } - // No reduction should happen here if r was reduced - None => return Err(()), - }; - } - - #[allow(non_snake_case)] - let R = AffinePoint::decompress(&r_bytes, u8::from(recovery_id.is_y_odd()).into()); - - if R.is_none().into() { - return Err(()); - } - - #[allow(non_snake_case)] - let R = ProjectivePoint::from(R.unwrap()); - let r_inv: Scalar = *r.invert(); - let u1 = -(r_inv * z); - let u2 = r_inv * *s; - let pk = ProjectivePoint::lincomb(&ProjectivePoint::GENERATOR, &u1, &R, &u2); - let vk = VerifyingKey::from_affine(pk.into()).map_err(|_| ())?; - - // Ensure signature verifies with the recovered key - let field = bits2field::(digest).map_err(|_| ())?; - // here we actually skip a high-s check (that should never be there at the first place and should be checked by caller) - let _ = k256::ecdsa::hazmat::verify_prehashed(&vk.as_affine().into(), &field, &signature) - .map_err(|_| ())?; - - Ok(vk) -} - -pub fn ecrecover_function( - monotonic_cycle_counter: u32, - precompile_call_params: LogQuery, - memory: &mut M, -) -> ( - usize, - Option<( - Vec, - Vec, - Vec, - )>, -) { - let mut processor = ECRecoverPrecompile::; - processor.execute_precompile(monotonic_cycle_counter, precompile_call_params, memory) -} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecrecover/airbender_backend.rs b/crates/zk_evm_abstractions/src/precompiles/ecrecover/airbender_backend.rs new file mode 100644 index 00000000..31ea2f4f --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecrecover/airbender_backend.rs @@ -0,0 +1,51 @@ +use zkevm_opcode_defs::k256; +use zkevm_opcode_defs::k256::ecdsa::VerifyingKey; + +use super::ECRecoverBackend; + +// ============================================================================== +// Delegated Backend +// ============================================================================== +pub(super) struct DelegatedECRecoverBackend; + +impl ECRecoverBackend for DelegatedECRecoverBackend { + fn recover( + digest: &[u8; 32], + r: &[u8; 32], + s: &[u8; 32], + rec_id: u8, + ) -> Result { + use airbender_crypto::k256::ecdsa::{RecoveryId, Signature}; + use airbender_crypto::k256::elliptic_curve::ops::Reduce; + use airbender_crypto::k256::{ecdsa::hazmat::bits2field, Scalar}; + use airbender_crypto::secp256k1; + + let signature = Signature::from_scalars(*r, *s).map_err(|_| ())?; + let recovery_id = RecoveryId::try_from(rec_id).unwrap(); + + let mut signature_bytes = [0u8; 64]; + signature_bytes[..32].copy_from_slice(r); + signature_bytes[32..].copy_from_slice(s); + let legacy_signature = + k256::ecdsa::Signature::try_from(&signature_bytes[..]).map_err(|_| ())?; + + let message = >::reduce_bytes( + &bits2field::(digest).map_err(|_| ())?, + ); + + let recovered_key = + secp256k1::recover(&message, &signature, &recovery_id).map_err(|_| ())?; + let encoded = recovered_key.to_encoded_point(false); + let verifying_key = VerifyingKey::from_sec1_bytes(encoded.as_bytes()).map_err(|_| ())?; + + let field = k256::ecdsa::hazmat::bits2field::(digest).map_err(|_| ())?; + let _ = k256::ecdsa::hazmat::verify_prehashed( + &verifying_key.as_affine().into(), + &field, + &legacy_signature, + ) + .map_err(|_| ())?; + + Ok(verifying_key) + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecrecover/legacy_backend.rs b/crates/zk_evm_abstractions/src/precompiles/ecrecover/legacy_backend.rs new file mode 100644 index 00000000..7d26bb2d --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecrecover/legacy_backend.rs @@ -0,0 +1,92 @@ +use zkevm_opcode_defs::k256; +use zkevm_opcode_defs::k256::ecdsa::VerifyingKey; + +use super::ECRecoverBackend; + +// ============================================================================== +// Legacy Backend +// ============================================================================== +pub(super) struct LegacyECRecoverBackend; + +impl ECRecoverBackend for LegacyECRecoverBackend { + fn recover( + digest: &[u8; 32], + r: &[u8; 32], + s: &[u8; 32], + rec_id: u8, + ) -> Result { + use k256::ecdsa::{RecoveryId, Signature}; + + let mut signature = [0u8; 64]; + signature[..32].copy_from_slice(r); + signature[32..].copy_from_slice(s); + let signature = Signature::try_from(&signature[..]).map_err(|_| ())?; + let recovery_id = RecoveryId::try_from(rec_id).unwrap(); + + recover_no_malleability_check(digest, signature, recovery_id) + } +} + +fn recover_no_malleability_check( + digest: &[u8; 32], + signature: k256::ecdsa::Signature, + recovery_id: k256::ecdsa::RecoveryId, +) -> Result { + use k256::ecdsa::hazmat::bits2field; + use k256::elliptic_curve::bigint::CheckedAdd; + use k256::elliptic_curve::generic_array::GenericArray; + use k256::elliptic_curve::ops::Invert; + use k256::elliptic_curve::ops::LinearCombination; + use k256::elliptic_curve::ops::Reduce; + use k256::elliptic_curve::point::DecompressPoint; + use k256::elliptic_curve::Curve; + use k256::elliptic_curve::FieldBytesEncoding; + use k256::elliptic_curve::PrimeField; + use k256::AffinePoint; + use k256::ProjectivePoint; + use k256::Scalar; + + let (r, s) = signature.split_scalars(); + let z = >::reduce_bytes( + &bits2field::(digest).map_err(|_| ())?, + ); + + let mut r_bytes: GenericArray::FieldBytesSize> = r.to_repr(); + if recovery_id.is_x_reduced() { + match Option::::from( + >::decode_field_bytes(&r_bytes) + .checked_add(&k256::Secp256k1::ORDER), + ) { + Some(restored) => { + r_bytes = >::encode_field_bytes( + &restored, + ) + } + None => return Err(()), + }; + } + + let recovered_point = + AffinePoint::decompress(&r_bytes, u8::from(recovery_id.is_y_odd()).into()); + if recovered_point.is_none().into() { + return Err(()); + } + + let recovered_point = ProjectivePoint::from(recovered_point.unwrap()); + let r_inv: Scalar = *r.invert(); + let u1 = -(r_inv * z); + let u2 = r_inv * *s; + let public_key = + ProjectivePoint::lincomb(&ProjectivePoint::GENERATOR, &u1, &recovered_point, &u2); + let verifying_key = VerifyingKey::from_affine(public_key.into()).map_err(|_| ())?; + + let field = bits2field::(digest).map_err(|_| ())?; + let _ = k256::ecdsa::hazmat::verify_prehashed( + &verifying_key.as_affine().into(), + &field, + &signature, + ) + .map_err(|_| ())?; + + Ok(verifying_key) +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecrecover/mod.rs b/crates/zk_evm_abstractions/src/precompiles/ecrecover/mod.rs new file mode 100644 index 00000000..2bd9b6e7 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecrecover/mod.rs @@ -0,0 +1,207 @@ +use cfg_if::cfg_if; +use zkevm_opcode_defs::k256::ecdsa::VerifyingKey; +use zkevm_opcode_defs::sha2::Digest; +use zkevm_opcode_defs::{ethereum_types::U256, sha3}; + +use super::*; + +#[cfg(feature = "airbender-precompile-delegations")] +mod airbender_backend; +#[cfg(any(not(feature = "airbender-precompile-delegations"), test))] +mod legacy_backend; +#[cfg(test)] +mod tests; + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use self::airbender_backend::DelegatedECRecoverBackend as ActiveECRecoverBackend; + } else { + use self::legacy_backend::LegacyECRecoverBackend as ActiveECRecoverBackend; + } +} + +// We need hash, v, r, s. +pub const MEMORY_READS_PER_CYCLE: usize = 4; +pub const MEMORY_WRITES_PER_CYCLE: usize = 2; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ECRecoverRoundWitness { + pub new_request: LogQuery, + pub reads: [MemoryQuery; MEMORY_READS_PER_CYCLE], + pub writes: [MemoryQuery; MEMORY_WRITES_PER_CYCLE], +} + +trait ECRecoverBackend { + fn recover( + digest: &[u8; 32], + r: &[u8; 32], + s: &[u8; 32], + rec_id: u8, + ) -> Result; +} + +fn execute_ecrecover_precompile( + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<( + Vec, + Vec, + Vec, + )>, +) { + const NUM_ROUNDS: usize = 1; + + let precompile_call_params = query; + let params = precompile_abi_in_log(precompile_call_params); + let timestamp_to_read = precompile_call_params.timestamp; + let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); + + let mut current_read_location = MemoryLocation { + memory_type: MemoryType::Heap, + page: MemoryPage(params.memory_page_to_read), + index: MemoryIndex(params.input_memory_offset), + }; + + let mut read_history = if B { + Vec::with_capacity(MEMORY_READS_PER_CYCLE) + } else { + vec![] + }; + let mut write_history = if B { + Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) + } else { + vec![] + }; + + let mut round_witness = ECRecoverRoundWitness { + new_request: precompile_call_params, + reads: [MemoryQuery::empty(); MEMORY_READS_PER_CYCLE], + writes: [MemoryQuery::empty(); MEMORY_WRITES_PER_CYCLE], + }; + + let mut input_words = [U256::zero(); MEMORY_READS_PER_CYCLE]; + for (idx, input_word) in input_words.iter_mut().enumerate() { + let read_query = MemoryQuery { + timestamp: timestamp_to_read, + location: current_read_location, + value: U256::zero(), + value_is_pointer: false, + rw_flag: false, + }; + let read_query = memory.execute_partial_query(monotonic_cycle_counter, read_query); + *input_word = read_query.value; + + if B { + round_witness.reads[idx] = read_query; + read_history.push(read_query); + } + + current_read_location.index.0 += 1; + } + + let digest = u256_to_bytes32(input_words[0]); + let rec_id_bytes = u256_to_bytes32(input_words[1]); + let r = u256_to_bytes32(input_words[2]); + let s = u256_to_bytes32(input_words[3]); + let rec_id = rec_id_bytes[31]; + assert!(rec_id == 0 || rec_id == 1); + + let result = Backend::recover(&digest, &r, &s, rec_id); + let output_values = match result { + Ok(verifying_key) => [U256::one(), verifying_key_to_address(&verifying_key)], + Err(_) => [U256::zero(), U256::zero()], + }; + + let mut write_location = MemoryLocation { + memory_type: MemoryType::Heap, + page: MemoryPage(params.memory_page_to_write), + index: MemoryIndex(params.output_memory_offset), + }; + + for (idx, value) in output_values.into_iter().enumerate() { + let write_query = MemoryQuery { + timestamp: timestamp_to_write, + location: write_location, + value, + value_is_pointer: false, + rw_flag: true, + }; + let write_query = memory.execute_partial_query(monotonic_cycle_counter, write_query); + + if B { + round_witness.writes[idx] = write_query; + write_history.push(write_query); + } + + write_location.index.0 += 1; + } + + let witness = if B { + Some((read_history, write_history, vec![round_witness])) + } else { + None + }; + + (NUM_ROUNDS, witness) +} + +fn u256_to_bytes32(value: U256) -> [u8; 32] { + let mut bytes = [0u8; 32]; + value.to_big_endian(&mut bytes); + bytes +} + +fn verifying_key_to_address(verifying_key: &VerifyingKey) -> U256 { + let encoded = verifying_key.to_encoded_point(false); + let encoded_ref = encoded.as_bytes(); + let address_hash = sha3::Keccak256::digest(&encoded_ref[1..]); + + let mut address = [0u8; 32]; + address[12..].copy_from_slice(&address_hash.as_slice()[12..]); + U256::from_big_endian(&address) +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ECRecoverPrecompile; + +impl Precompile for ECRecoverPrecompile { + type CycleWitness = ECRecoverRoundWitness; + + fn execute_precompile( + &mut self, + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, + ) -> ( + usize, + Option<(Vec, Vec, Vec)>, + ) { + execute_ecrecover_precompile::( + monotonic_cycle_counter, + query, + memory, + ) + } +} + +pub fn ecrecover_function( + monotonic_cycle_counter: u32, + precompile_call_params: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<( + Vec, + Vec, + Vec, + )>, +) { + execute_ecrecover_precompile::( + monotonic_cycle_counter, + precompile_call_params, + memory, + ) +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecrecover/tests/mod.rs b/crates/zk_evm_abstractions/src/precompiles/ecrecover/tests/mod.rs new file mode 100644 index 00000000..b76aae40 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecrecover/tests/mod.rs @@ -0,0 +1,81 @@ +use cfg_if::cfg_if; +use quickcheck::QuickCheck; + +use super::legacy_backend::LegacyECRecoverBackend; + +mod utils; + +use self::utils::{ + assert_backend_matches_case, deterministic_ecrecover_cases, high_s_ecrecover_cases, + legacy_backend_matches_signed_message, QUICKCHECK_MAX_MESSAGE_BYTES, QUICKCHECK_NUM_CASES, +}; + +#[test] +fn legacy_backend_matches_static_vectors() { + for case in deterministic_ecrecover_cases() { + assert_backend_matches_case::(&case); + } +} + +#[test] +fn legacy_backend_accepts_high_s_signature_vectors() { + for case in high_s_ecrecover_cases() { + assert_backend_matches_case::(&case); + } +} + +#[test] +fn legacy_backend_matches_signing_reference_quickcheck() { + fn property(mut message: Vec) -> bool { + message.truncate(QUICKCHECK_MAX_MESSAGE_BYTES); + legacy_backend_matches_signed_message::(&message) + } + + QuickCheck::new() + .tests(QUICKCHECK_NUM_CASES) + .quickcheck(property as fn(Vec) -> bool); +} + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use super::airbender_backend::DelegatedECRecoverBackend; + use self::utils::{ + delegated_backend_matches_signed_message, invalid_recovery_id_panics_like_legacy, + }; + + #[test] + fn delegated_backend_matches_static_vectors() { + for case in deterministic_ecrecover_cases() { + assert_backend_matches_case::(&case); + } + } + + #[test] + fn delegated_backend_accepts_high_s_signature_vectors() { + for case in high_s_ecrecover_cases() { + assert_backend_matches_case::(&case); + } + } + + #[test] + fn delegated_backend_matches_legacy_quickcheck() { + fn property(mut message: Vec) -> bool { + message.truncate(QUICKCHECK_MAX_MESSAGE_BYTES); + legacy_backend_matches_signed_message::(&message) + && delegated_backend_matches_signed_message::(&message) + } + + QuickCheck::new() + .tests(QUICKCHECK_NUM_CASES) + .quickcheck(property as fn(Vec) -> bool); + } + + #[test] + fn invalid_recovery_id_matches_legacy_panics() { + assert!(invalid_recovery_id_panics_like_legacy::< + LegacyECRecoverBackend, + DelegatedECRecoverBackend, + >()); + } + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/ecrecover/tests/utils.rs b/crates/zk_evm_abstractions/src/precompiles/ecrecover/tests/utils.rs new file mode 100644 index 00000000..8a6b6e8f --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/ecrecover/tests/utils.rs @@ -0,0 +1,267 @@ +use cfg_if::cfg_if; +use zkevm_opcode_defs::ethereum_types::U256; +use zkevm_opcode_defs::k256::ecdsa::SigningKey; +use zkevm_opcode_defs::k256::ecdsa::VerifyingKey; +use zkevm_opcode_defs::sha2::Digest; +use zkevm_opcode_defs::sha3; + +use super::super::ECRecoverBackend; + +pub(super) const QUICKCHECK_NUM_CASES: u64 = 256; +pub(super) const QUICKCHECK_MAX_MESSAGE_BYTES: usize = 256; + +pub(super) struct DeterministicECRecoverCase { + name: &'static str, + digest: [u8; 32], + r: [u8; 32], + s: [u8; 32], + rec_id: u8, + expected: Result<[u8; 65], ()>, +} + +pub(super) fn deterministic_ecrecover_cases() -> Vec { + let valid_cases = [ + ( + "empty-message-fixed-key", + hex_to_32("06f9f7f6f4c5f70b2bcf0fdb5f8f4672d8cc9b2f4fbed4352f0f0d0c0b0a0908"), + b"".as_slice(), + ), + ( + "airbender-message-fixed-key", + hex_to_32("49a3f7e1d4c6b8a2908172635445362718190a0b0c0d0e0f1021324354657687"), + b"airbender".as_slice(), + ), + ( + "protocol-message-fixed-key", + hex_to_32("8854b52e0d56cb713f1189b15fd3684670e8c89ce11b7bcff37204d894f2519a"), + b"zksync-protocol ecrecover differential test".as_slice(), + ), + ] + .into_iter() + .map(|(name, private_key, message)| signed_case(name, private_key, message)); + + let invalid_cases = [ + DeterministicECRecoverCase { + name: "all-zero-inputs", + digest: hex_to_32("0000000000000000000000000000000000000000000000000000000000000000"), + r: hex_to_32("0000000000000000000000000000000000000000000000000000000000000000"), + s: hex_to_32("0000000000000000000000000000000000000000000000000000000000000000"), + rec_id: 0, + expected: Err(()), + }, + DeterministicECRecoverCase { + name: "all-ones-inputs", + digest: hex_to_32("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + r: hex_to_32("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + s: hex_to_32("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + rec_id: 1, + expected: Err(()), + }, + ]; + + valid_cases.chain(invalid_cases).collect() +} + +pub(super) fn high_s_ecrecover_cases() -> Vec { + [ + ( + "empty-message-high-s", + hex_to_32("06f9f7f6f4c5f70b2bcf0fdb5f8f4672d8cc9b2f4fbed4352f0f0d0c0b0a0908"), + b"".as_slice(), + ), + ( + "airbender-message-high-s", + hex_to_32("49a3f7e1d4c6b8a2908172635445362718190a0b0c0d0e0f1021324354657687"), + b"airbender".as_slice(), + ), + ( + "protocol-message-high-s", + hex_to_32("8854b52e0d56cb713f1189b15fd3684670e8c89ce11b7bcff37204d894f2519a"), + b"zksync-protocol ecrecover differential test".as_slice(), + ), + ] + .into_iter() + .map(|(name, private_key, message)| high_s_signed_case(name, private_key, message)) + .collect() +} + +pub(super) fn assert_backend_matches_case( + case: &DeterministicECRecoverCase, +) { + let actual = Backend::recover(&case.digest, &case.r, &case.s, case.rec_id); + + match (&case.expected, actual) { + (Ok(expected), Ok(actual)) => assert_eq!( + verifying_key_bytes(&actual), + *expected, + "backend must match static vector '{}'", + case.name, + ), + (Err(_), Err(_)) => {} + (Ok(_), Err(_)) => panic!("backend unexpectedly failed for vector '{}'", case.name), + (Err(_), Ok(actual)) => panic!( + "backend unexpectedly succeeded for vector '{}': {:?}", + case.name, + verifying_key_bytes(&actual), + ), + } +} + +pub(super) fn legacy_backend_matches_signed_message( + message: &[u8], +) -> bool { + let case = signed_case( + "quickcheck-message", + hex_to_32("06f9f7f6f4c5f70b2bcf0fdb5f8f4672d8cc9b2f4fbed4352f0f0d0c0b0a0908"), + message, + ); + backend_matches_case::(&case) +} + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + pub(super) fn delegated_backend_matches_signed_message( + message: &[u8], + ) -> bool { + let case = signed_case( + "quickcheck-message", + hex_to_32("49a3f7e1d4c6b8a2908172635445362718190a0b0c0d0e0f1021324354657687"), + message, + ); + backend_matches_case::(&case) + } + + pub(super) fn invalid_recovery_id_panics_like_legacy() -> bool + where + Legacy: ECRecoverBackend, + Delegated: ECRecoverBackend, + { + let digest = hex_to_32("0101010101010101010101010101010101010101010101010101010101010101"); + let r = hex_to_32("0202020202020202020202020202020202020202020202020202020202020202"); + let s = hex_to_32("0303030303030303030303030303030303030303030303030303030303030303"); + + let legacy = std::panic::catch_unwind(|| Legacy::recover(&digest, &r, &s, 2)); + let delegated = std::panic::catch_unwind(|| Delegated::recover(&digest, &r, &s, 2)); + + legacy.is_err() == delegated.is_err() + } + } +} + +fn backend_matches_case(case: &DeterministicECRecoverCase) -> bool { + match ( + &case.expected, + Backend::recover(&case.digest, &case.r, &case.s, case.rec_id), + ) { + (Ok(expected), Ok(actual)) => verifying_key_bytes(&actual) == *expected, + (Err(_), Err(_)) => true, + _ => false, + } +} + +fn signed_case( + name: &'static str, + private_key: [u8; 32], + message: &[u8], +) -> DeterministicECRecoverCase { + let signing_key = + SigningKey::from_bytes((&private_key).into()).expect("private key vector must be valid"); + let digest = sha3::Keccak256::digest(message); + + let mut digest_bytes = [0u8; 32]; + digest_bytes.copy_from_slice(digest.as_slice()); + + let (signature, recovery_id) = signing_key + .sign_prehash_recoverable(&digest_bytes) + .expect("prehash signing must succeed"); + + let signature_bytes = signature.to_bytes(); + let mut r = [0u8; 32]; + let mut s = [0u8; 32]; + r.copy_from_slice(&signature_bytes[..32]); + s.copy_from_slice(&signature_bytes[32..]); + + DeterministicECRecoverCase { + name, + digest: digest_bytes, + r, + s, + rec_id: recovery_id.to_byte(), + expected: Ok(verifying_key_bytes(signing_key.verifying_key())), + } +} + +fn high_s_signed_case( + name: &'static str, + private_key: [u8; 32], + message: &[u8], +) -> DeterministicECRecoverCase { + use zkevm_opcode_defs::k256::ecdsa::{RecoveryId, Signature}; + + let signing_key = + SigningKey::from_bytes((&private_key).into()).expect("private key vector must be valid"); + let digest = sha3::Keccak256::digest(message); + + let mut digest_bytes = [0u8; 32]; + digest_bytes.copy_from_slice(digest.as_slice()); + + let (signature, recovery_id) = signing_key + .sign_prehash_recoverable(&digest_bytes) + .expect("prehash signing must succeed"); + + let (r_bytes, s_bytes) = signature.split_bytes(); + let r: [u8; 32] = r_bytes.into(); + let low_s: [u8; 32] = s_bytes.into(); + let high_s = to_high_s_bytes(low_s); + + let high_s_signature = + Signature::from_scalars(r, high_s).expect("constructed high-s signature must be valid"); + let normalized_signature = high_s_signature + .normalize_s() + .expect("constructed signature must normalize back to low-s form"); + assert_eq!( + normalized_signature.to_bytes().as_slice(), + signature.to_bytes().as_slice(), + "high-s test vector must be the malleable counterpart of the original signature", + ); + + let high_s_recovery_id = RecoveryId::new(!recovery_id.is_y_odd(), recovery_id.is_x_reduced()); + + DeterministicECRecoverCase { + name, + digest: digest_bytes, + r, + s: high_s, + rec_id: high_s_recovery_id.to_byte(), + expected: Ok(verifying_key_bytes(signing_key.verifying_key())), + } +} + +fn to_high_s_bytes(low_s: [u8; 32]) -> [u8; 32] { + let curve_order = U256::from_str_radix( + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + 16, + ) + .expect("secp256k1 group order constant must parse"); + let low_s = U256::from_big_endian(&low_s); + let high_s = curve_order - low_s; + + let mut bytes = [0u8; 32]; + high_s.to_big_endian(&mut bytes); + bytes +} + +fn verifying_key_bytes(verifying_key: &VerifyingKey) -> [u8; 65] { + let encoded = verifying_key.to_encoded_point(false); + let mut bytes = [0u8; 65]; + bytes.copy_from_slice(encoded.as_bytes()); + bytes +} + +fn hex_to_32(hex_str: &str) -> [u8; 32] { + let bytes = hex::decode(hex_str).expect("hex decode should succeed"); + bytes + .as_slice() + .try_into() + .expect("hex string must be 32 bytes") +} diff --git a/crates/zk_evm_abstractions/src/precompiles/keccak256.rs b/crates/zk_evm_abstractions/src/precompiles/keccak256.rs deleted file mode 100644 index 22471e44..00000000 --- a/crates/zk_evm_abstractions/src/precompiles/keccak256.rs +++ /dev/null @@ -1,321 +0,0 @@ -use zkevm_opcode_defs::ethereum_types::U256; -pub use zkevm_opcode_defs::sha2::Digest; -pub use zkevm_opcode_defs::sha3::Keccak256; - -use crate::aux::*; -use crate::queries::*; -use crate::vm::*; - -use super::precompile_abi_in_log; - -pub const KECCAK_RATE_BYTES: usize = 136; -pub const MEMORY_READS_PER_CYCLE: usize = 6; -pub const KECCAK_PRECOMPILE_BUFFER_SIZE: usize = MEMORY_READS_PER_CYCLE * 32; -pub const MEMORY_WRITES_PER_CYCLE: usize = 1; -pub const NUM_WORDS_PER_QUERY: usize = 4; -pub const KECCAK_RATE_IN_U64_WORDS: usize = KECCAK_RATE_BYTES / 8; - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Keccak256RoundWitness { - pub new_request: Option, - pub reads: [Option; MEMORY_READS_PER_CYCLE], - pub writes: Option<[MemoryQuery; MEMORY_WRITES_PER_CYCLE]>, -} - -pub struct ByteBuffer { - pub bytes: [u8; BUFFER_SIZE], - pub filled: usize, -} - -impl ByteBuffer { - pub fn can_fill_bytes(&self, num_bytes: usize) -> bool { - self.filled + num_bytes <= BUFFER_SIZE - } - - pub fn fill_with_bytes( - &mut self, - input: &[u8; N], - offset: usize, - meaningful_bytes: usize, - ) { - assert!(self.filled + meaningful_bytes <= BUFFER_SIZE); - self.bytes[self.filled..(self.filled + meaningful_bytes)] - .copy_from_slice(&input[offset..(offset + meaningful_bytes)]); - self.filled += meaningful_bytes; - } - - pub fn consume(&mut self) -> [u8; N] { - assert!(N <= BUFFER_SIZE); - let mut result = [0u8; N]; - result.copy_from_slice(&self.bytes[..N]); - if self.filled < N { - self.filled = 0; - } else { - self.filled -= N; - } - let mut new_bytes = [0u8; BUFFER_SIZE]; - new_bytes[..(BUFFER_SIZE - N)].copy_from_slice(&self.bytes[N..]); - self.bytes = new_bytes; - - result - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Keccak256Precompile; - -impl Precompile for Keccak256Precompile { - type CycleWitness = Keccak256RoundWitness; - - fn execute_precompile( - &mut self, - monotonic_cycle_counter: u32, - query: LogQuery, - memory: &mut M, - ) -> ( - usize, - Option<(Vec, Vec, Vec)>, - ) { - let mut full_round_padding = [0u8; KECCAK_RATE_BYTES]; - full_round_padding[0] = 0x01; - full_round_padding[KECCAK_RATE_BYTES - 1] = 0x80; - - let precompile_call_params = query; - // read the parameters - let params = precompile_abi_in_log(precompile_call_params); - let timestamp_to_read = precompile_call_params.timestamp; - let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); // our default timestamping agreement - - let mut input_byte_offset = params.input_memory_offset as usize; - let mut bytes_left = params.input_memory_length as usize; - - let mut num_rounds = (bytes_left + (KECCAK_RATE_BYTES - 1)) / KECCAK_RATE_BYTES; - let padding_space = bytes_left % KECCAK_RATE_BYTES; - let needs_extra_padding_round = padding_space == 0; - if needs_extra_padding_round { - num_rounds += 1; - } - - let source_memory_page = params.memory_page_to_read; - let destination_memory_page = params.memory_page_to_write; - let write_offset = params.output_memory_offset; - - let mut read_queries = if B { - Vec::with_capacity(MEMORY_READS_PER_CYCLE * num_rounds) - } else { - vec![] - }; - - let mut write_queries = if B { - Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) - } else { - vec![] - }; - - let mut witness = if B { - Vec::with_capacity(num_rounds) - } else { - vec![] - }; - - let mut input_buffer = ByteBuffer:: { - bytes: [0u8; KECCAK_PRECOMPILE_BUFFER_SIZE], - filled: 0, - }; - - let mut internal_state = Keccak256::default(); - - for round in 0..num_rounds { - let mut round_witness = Keccak256RoundWitness { - new_request: None, - reads: [None; MEMORY_READS_PER_CYCLE], - writes: None, - }; - - if B && round == 0 { - round_witness.new_request = Some(precompile_call_params); - } - - let is_last = round == num_rounds - 1; - let paddings_round = needs_extra_padding_round && is_last; - - let mut bytes32_buffer = [0u8; 32]; - for idx in 0..MEMORY_READS_PER_CYCLE { - let (memory_index, unalignment) = (input_byte_offset / 32, input_byte_offset % 32); - let at_most_meaningful_bytes_in_query = 32 - unalignment; - let meaningful_bytes_in_query = if bytes_left >= at_most_meaningful_bytes_in_query { - at_most_meaningful_bytes_in_query - } else { - bytes_left - }; - - let enough_buffer_space = input_buffer.can_fill_bytes(meaningful_bytes_in_query); - let nothing_to_read = meaningful_bytes_in_query == 0; - let should_read = - nothing_to_read == false && paddings_round == false && enough_buffer_space; - - let bytes_to_fill = if should_read { - meaningful_bytes_in_query - } else { - 0 - }; - - if should_read { - input_byte_offset += meaningful_bytes_in_query; - bytes_left -= meaningful_bytes_in_query; - - let data_query = MemoryQuery { - timestamp: timestamp_to_read, - location: MemoryLocation { - memory_type: MemoryType::FatPointer, - page: MemoryPage(source_memory_page), - index: MemoryIndex(memory_index as u32), - }, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let data_query = - memory.execute_partial_query(monotonic_cycle_counter, data_query); - let data = data_query.value; - if B { - round_witness.reads[idx] = Some(data_query); - read_queries.push(data_query); - } - data.to_big_endian(&mut bytes32_buffer[..]); - } - - input_buffer.fill_with_bytes(&bytes32_buffer, unalignment, bytes_to_fill) - } - - // buffer is always large enough for us to have data - - let mut block = input_buffer.consume::(); - // apply padding - if paddings_round { - block = full_round_padding; - } else if is_last { - if padding_space == KECCAK_RATE_BYTES - 1 { - block[KECCAK_RATE_BYTES - 1] = 0x81; - } else { - block[padding_space] = 0x01; - block[KECCAK_RATE_BYTES - 1] = 0x80; - } - } - // update the keccak internal state - internal_state.update(&block); - - if is_last { - let state_inner = transmute_state(internal_state.clone()); - - // take hash and properly set endianess for the output word - let mut hash_as_bytes32 = [0u8; 32]; - hash_as_bytes32[0..8].copy_from_slice(&state_inner[0].to_le_bytes()); - hash_as_bytes32[8..16].copy_from_slice(&state_inner[1].to_le_bytes()); - hash_as_bytes32[16..24].copy_from_slice(&state_inner[2].to_le_bytes()); - hash_as_bytes32[24..32].copy_from_slice(&state_inner[3].to_le_bytes()); - let as_u256 = U256::from_big_endian(&hash_as_bytes32); - let write_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(destination_memory_page), - index: MemoryIndex(write_offset), - }; - - let result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: as_u256, - value_is_pointer: false, - rw_flag: true, - }; - - let result_query = - memory.execute_partial_query(monotonic_cycle_counter, result_query); - - if B { - round_witness.writes = Some([result_query]); - write_queries.push(result_query); - } - } - - if B { - witness.push(round_witness); - } - } - - let witness = if B { - Some((read_queries, write_queries, witness)) - } else { - None - }; - - (num_rounds, witness) - } -} - -pub fn keccak256_rounds_function( - monotonic_cycle_counter: u32, - precompile_call_params: LogQuery, - memory: &mut M, -) -> ( - usize, - Option<( - Vec, - Vec, - Vec, - )>, -) { - let mut processor = Keccak256Precompile::; - processor.execute_precompile(monotonic_cycle_counter, precompile_call_params, memory) -} - -pub type Keccak256InnerState = [u64; 25]; - -struct Sha3State { - state: [u64; 25], - _round_count: usize, -} - -struct BlockBuffer { - _buffer: [u8; 136], - _pos: u8, -} - -struct CoreWrapper { - core: Sha3State, - _buffer: BlockBuffer, -} - -static_assertions::assert_eq_size!(Keccak256, CoreWrapper); - -pub fn transmute_state(reference_state: Keccak256) -> Keccak256InnerState { - // we use a trick that size of both structures is the same, and even though we do not know a stable field layout, - // we can replicate it - let our_wrapper: CoreWrapper = unsafe { std::mem::transmute(reference_state) }; - - our_wrapper.core.state -} - -#[cfg(test)] -mod tests { - use super::*; - use zkevm_opcode_defs::sha2::Digest; - - #[test] - fn test_empty_string() { - let mut hasher = Keccak256::new(); - hasher.update(&[]); - let result = hasher.finalize(); - println!("Empty string hash = {}", hex::encode(result.as_slice())); - - let mut our_hasher = Keccak256::default(); - let mut block = [0u8; 136]; - block[0] = 0x01; - block[135] = 0x80; - our_hasher.update(&block); - let state_inner = transmute_state(our_hasher); - for (idx, el) in state_inner.iter().enumerate() { - println!("Element {} = 0x{:016x}", idx, el); - } - } -} diff --git a/crates/zk_evm_abstractions/src/precompiles/keccak256/airbender_backend.rs b/crates/zk_evm_abstractions/src/precompiles/keccak256/airbender_backend.rs new file mode 100644 index 00000000..2a229b68 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/keccak256/airbender_backend.rs @@ -0,0 +1,61 @@ +use airbender_crypto::sha3::Keccak256 as AirbenderKeccak256; + +use super::{KeccakBackend, KECCAK_PRECOMPILE_BUFFER_SIZE, KECCAK_RATE_BYTES}; + +// ============================================================================== +// Delegated Backend +// ============================================================================== +// +// The delegated backend must preserve the legacy read cadence so witnesses and +// cycle accounting stay stable. Once bytes have been forwarded to the delegated +// digest, re-buffering them locally does not buy us anything. +pub(super) struct DelegatedKeccakBackend { + buffered_bytes: usize, + state: Option, +} + +impl KeccakBackend for DelegatedKeccakBackend { + fn new() -> Self { + Self { + buffered_bytes: 0, + state: Some(::new()), + } + } + + fn can_fill_bytes(&self, num_bytes: usize) -> bool { + self.buffered_bytes + num_bytes <= KECCAK_PRECOMPILE_BUFFER_SIZE + } + + fn absorb_query_bytes(&mut self, input: &[u8; 32], offset: usize, meaningful_bytes: usize) { + if meaningful_bytes == 0 { + return; + } + + let end = offset + meaningful_bytes; + airbender_crypto::MiniDigest::update( + self.state + .as_mut() + .expect("airbender keccak state must exist before finalization"), + &input[offset..end], + ); + self.buffered_bytes += meaningful_bytes; + } + + fn finish_round( + &mut self, + _full_round_padding: &[u8; KECCAK_RATE_BYTES], + _is_last: bool, + _paddings_round: bool, + _padding_space: usize, + ) { + self.buffered_bytes = self.buffered_bytes.saturating_sub(KECCAK_RATE_BYTES); + } + + fn finalize(&mut self) -> [u8; 32] { + airbender_crypto::MiniDigest::finalize( + self.state + .take() + .expect("airbender keccak state must exist for finalization"), + ) + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/keccak256/legacy_backend.rs b/crates/zk_evm_abstractions/src/precompiles/keccak256/legacy_backend.rs new file mode 100644 index 00000000..a3b66d46 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/keccak256/legacy_backend.rs @@ -0,0 +1,75 @@ +use super::{ + transmute_state, ByteBuffer, Digest, Keccak256, KeccakBackend, KECCAK_PRECOMPILE_BUFFER_SIZE, + KECCAK_RATE_BYTES, +}; + +// ============================================================================== +// Legacy Backend +// ============================================================================== +// +// The legacy implementation absorbs complete rate-sized blocks, so it keeps the +// explicit staging buffer that assembles aligned memory reads into keccak rounds. +pub(super) struct LegacyKeccakBackend { + buffer: ByteBuffer, + state: Keccak256, +} + +impl LegacyKeccakBackend { + fn finalize_state(state: Keccak256) -> [u8; 32] { + let state_inner = transmute_state(state); + + // Take the first four lanes and serialize them into the canonical digest bytes. + let mut hash_as_bytes32 = [0u8; 32]; + hash_as_bytes32[0..8].copy_from_slice(&state_inner[0].to_le_bytes()); + hash_as_bytes32[8..16].copy_from_slice(&state_inner[1].to_le_bytes()); + hash_as_bytes32[16..24].copy_from_slice(&state_inner[2].to_le_bytes()); + hash_as_bytes32[24..32].copy_from_slice(&state_inner[3].to_le_bytes()); + hash_as_bytes32 + } +} + +impl KeccakBackend for LegacyKeccakBackend { + fn new() -> Self { + Self { + buffer: ByteBuffer { + bytes: [0u8; KECCAK_PRECOMPILE_BUFFER_SIZE], + filled: 0, + }, + state: Keccak256::default(), + } + } + + fn can_fill_bytes(&self, num_bytes: usize) -> bool { + self.buffer.can_fill_bytes(num_bytes) + } + + fn absorb_query_bytes(&mut self, input: &[u8; 32], offset: usize, meaningful_bytes: usize) { + self.buffer.fill_with_bytes(input, offset, meaningful_bytes); + } + + fn finish_round( + &mut self, + full_round_padding: &[u8; KECCAK_RATE_BYTES], + is_last: bool, + paddings_round: bool, + padding_space: usize, + ) { + let mut block = self.buffer.consume::(); + if paddings_round { + block = *full_round_padding; + } else if is_last { + if padding_space == KECCAK_RATE_BYTES - 1 { + block[KECCAK_RATE_BYTES - 1] = 0x81; + } else { + block[padding_space] = 0x01; + block[KECCAK_RATE_BYTES - 1] = 0x80; + } + } + + self.state.update(&block); + } + + fn finalize(&mut self) -> [u8; 32] { + Self::finalize_state(std::mem::take(&mut self.state)) + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/keccak256/mod.rs b/crates/zk_evm_abstractions/src/precompiles/keccak256/mod.rs new file mode 100644 index 00000000..843b9261 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/keccak256/mod.rs @@ -0,0 +1,334 @@ +use cfg_if::cfg_if; +use zkevm_opcode_defs::ethereum_types::U256; +pub use zkevm_opcode_defs::sha2::Digest; +pub use zkevm_opcode_defs::sha3::Keccak256; + +use crate::aux::*; +use crate::queries::*; +use crate::vm::*; + +use super::precompile_abi_in_log; + +#[cfg(feature = "airbender-precompile-delegations")] +mod airbender_backend; +// When compiling tests, we want legacy impl to be available to compare against. +#[cfg(any(not(feature = "airbender-precompile-delegations"), test))] +mod legacy_backend; +#[cfg(test)] +mod tests; + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use self::airbender_backend::DelegatedKeccakBackend as ActiveKeccakBackend; + } else { + use self::legacy_backend::LegacyKeccakBackend as ActiveKeccakBackend; + } +} + +pub const KECCAK_RATE_BYTES: usize = 136; +pub const MEMORY_READS_PER_CYCLE: usize = 6; +pub const KECCAK_PRECOMPILE_BUFFER_SIZE: usize = MEMORY_READS_PER_CYCLE * 32; +pub const MEMORY_WRITES_PER_CYCLE: usize = 1; +pub const NUM_WORDS_PER_QUERY: usize = 4; +pub const KECCAK_RATE_IN_U64_WORDS: usize = KECCAK_RATE_BYTES / 8; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Keccak256RoundWitness { + pub new_request: Option, + pub reads: [Option; MEMORY_READS_PER_CYCLE], + pub writes: Option<[MemoryQuery; MEMORY_WRITES_PER_CYCLE]>, +} + +pub struct ByteBuffer { + pub bytes: [u8; BUFFER_SIZE], + pub filled: usize, +} + +impl ByteBuffer { + pub fn can_fill_bytes(&self, num_bytes: usize) -> bool { + self.filled + num_bytes <= BUFFER_SIZE + } + + pub fn fill_with_bytes( + &mut self, + input: &[u8; N], + offset: usize, + meaningful_bytes: usize, + ) { + assert!(self.filled + meaningful_bytes <= BUFFER_SIZE); + self.bytes[self.filled..(self.filled + meaningful_bytes)] + .copy_from_slice(&input[offset..(offset + meaningful_bytes)]); + self.filled += meaningful_bytes; + } + + pub fn consume(&mut self) -> [u8; N] { + assert!(N <= BUFFER_SIZE); + let mut result = [0u8; N]; + result.copy_from_slice(&self.bytes[..N]); + if self.filled < N { + self.filled = 0; + } else { + self.filled -= N; + } + let mut new_bytes = [0u8; BUFFER_SIZE]; + new_bytes[..(BUFFER_SIZE - N)].copy_from_slice(&self.bytes[N..]); + self.bytes = new_bytes; + + result + } +} + +// ============================================================================== +// Keccak Backend Selection +// ============================================================================== +// +// The precompile loop is identical for both implementations. The only backend-specific +// behavior is how bytes are accumulated between reads and how the final digest state is +// materialized. We encode that seam in a trait so the production path picks one backend, +// while tests can instantiate both and compare them through the same executor. +trait KeccakBackend { + fn new() -> Self; + + fn can_fill_bytes(&self, num_bytes: usize) -> bool; + + fn absorb_query_bytes(&mut self, input: &[u8; 32], offset: usize, meaningful_bytes: usize); + + fn finish_round( + &mut self, + full_round_padding: &[u8; KECCAK_RATE_BYTES], + is_last: bool, + paddings_round: bool, + padding_space: usize, + ); + + fn finalize(&mut self) -> [u8; 32]; +} + +fn execute_keccak_precompile( + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<( + Vec, + Vec, + Vec, + )>, +) { + let mut full_round_padding = [0u8; KECCAK_RATE_BYTES]; + full_round_padding[0] = 0x01; + full_round_padding[KECCAK_RATE_BYTES - 1] = 0x80; + + let precompile_call_params = query; + // read the parameters + let params = precompile_abi_in_log(precompile_call_params); + let timestamp_to_read = precompile_call_params.timestamp; + let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); // our default timestamping agreement + + let mut input_byte_offset = params.input_memory_offset as usize; + let mut bytes_left = params.input_memory_length as usize; + + let mut num_rounds = (bytes_left + (KECCAK_RATE_BYTES - 1)) / KECCAK_RATE_BYTES; + let padding_space = bytes_left % KECCAK_RATE_BYTES; + let needs_extra_padding_round = padding_space == 0; + if needs_extra_padding_round { + num_rounds += 1; + } + + let source_memory_page = params.memory_page_to_read; + let destination_memory_page = params.memory_page_to_write; + let write_offset = params.output_memory_offset; + + let mut read_queries = if B { + Vec::with_capacity(MEMORY_READS_PER_CYCLE * num_rounds) + } else { + vec![] + }; + + let mut write_queries = if B { + Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) + } else { + vec![] + }; + + let mut witness = if B { + Vec::with_capacity(num_rounds) + } else { + vec![] + }; + + let mut round_accumulator = Backend::new(); + + for round in 0..num_rounds { + let mut round_witness = Keccak256RoundWitness { + new_request: None, + reads: [None; MEMORY_READS_PER_CYCLE], + writes: None, + }; + + if B && round == 0 { + round_witness.new_request = Some(precompile_call_params); + } + + let is_last = round == num_rounds - 1; + let paddings_round = needs_extra_padding_round && is_last; + + let mut bytes32_buffer = [0u8; 32]; + for idx in 0..MEMORY_READS_PER_CYCLE { + let (memory_index, unalignment) = (input_byte_offset / 32, input_byte_offset % 32); + let at_most_meaningful_bytes_in_query = 32 - unalignment; + let meaningful_bytes_in_query = if bytes_left >= at_most_meaningful_bytes_in_query { + at_most_meaningful_bytes_in_query + } else { + bytes_left + }; + + let enough_buffer_space = round_accumulator.can_fill_bytes(meaningful_bytes_in_query); + let nothing_to_read = meaningful_bytes_in_query == 0; + let should_read = + nothing_to_read == false && paddings_round == false && enough_buffer_space; + + let bytes_to_fill = if should_read { + meaningful_bytes_in_query + } else { + 0 + }; + + if should_read { + input_byte_offset += meaningful_bytes_in_query; + bytes_left -= meaningful_bytes_in_query; + + let data_query = MemoryQuery { + timestamp: timestamp_to_read, + location: MemoryLocation { + memory_type: MemoryType::FatPointer, + page: MemoryPage(source_memory_page), + index: MemoryIndex(memory_index as u32), + }, + value: U256::zero(), + value_is_pointer: false, + rw_flag: false, + }; + let data_query = memory.execute_partial_query(monotonic_cycle_counter, data_query); + let data = data_query.value; + if B { + round_witness.reads[idx] = Some(data_query); + read_queries.push(data_query); + } + data.to_big_endian(&mut bytes32_buffer[..]); + } + + round_accumulator.absorb_query_bytes(&bytes32_buffer, unalignment, bytes_to_fill); + } + + round_accumulator.finish_round(&full_round_padding, is_last, paddings_round, padding_space); + + if is_last { + let hash_as_bytes32 = round_accumulator.finalize(); + + let as_u256 = U256::from_big_endian(&hash_as_bytes32); + let write_location = MemoryLocation { + memory_type: MemoryType::Heap, // we default for some value, here it's not that important + page: MemoryPage(destination_memory_page), + index: MemoryIndex(write_offset), + }; + + let result_query = MemoryQuery { + timestamp: timestamp_to_write, + location: write_location, + value: as_u256, + value_is_pointer: false, + rw_flag: true, + }; + + let result_query = memory.execute_partial_query(monotonic_cycle_counter, result_query); + + if B { + round_witness.writes = Some([result_query]); + write_queries.push(result_query); + } + } + + if B { + witness.push(round_witness); + } + } + + let witness = if B { + Some((read_queries, write_queries, witness)) + } else { + None + }; + + (num_rounds, witness) +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Keccak256Precompile; + +impl Precompile for Keccak256Precompile { + type CycleWitness = Keccak256RoundWitness; + + fn execute_precompile( + &mut self, + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, + ) -> ( + usize, + Option<(Vec, Vec, Vec)>, + ) { + execute_keccak_precompile::( + monotonic_cycle_counter, + query, + memory, + ) + } +} + +pub fn keccak256_rounds_function( + monotonic_cycle_counter: u32, + precompile_call_params: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<( + Vec, + Vec, + Vec, + )>, +) { + execute_keccak_precompile::( + monotonic_cycle_counter, + precompile_call_params, + memory, + ) +} + +pub type Keccak256InnerState = [u64; 25]; + +struct Sha3State { + state: [u64; 25], + _round_count: usize, +} + +struct BlockBuffer { + _buffer: [u8; 136], + _pos: u8, +} + +struct CoreWrapper { + core: Sha3State, + _buffer: BlockBuffer, +} + +static_assertions::assert_eq_size!(Keccak256, CoreWrapper); + +pub fn transmute_state(reference_state: Keccak256) -> Keccak256InnerState { + // we use a trick that size of both structures is the same, and even though we do not know a stable field layout, + // we can replicate it + let our_wrapper: CoreWrapper = unsafe { std::mem::transmute(reference_state) }; + + our_wrapper.core.state +} diff --git a/crates/zk_evm_abstractions/src/precompiles/keccak256/tests/mod.rs b/crates/zk_evm_abstractions/src/precompiles/keccak256/tests/mod.rs new file mode 100644 index 00000000..d9747823 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/keccak256/tests/mod.rs @@ -0,0 +1,91 @@ +use super::{legacy_backend::LegacyKeccakBackend, transmute_state, Keccak256}; +use cfg_if::cfg_if; +use quickcheck::QuickCheck; +use zkevm_opcode_defs::sha2::Digest; + +mod utils; + +use self::utils::{ + assert_backend_matches_reference, execution_output_bytes, reference_keccak256, + run_keccak_precompile_test_backend, DETERMINISTIC_KECCAK_CASES, QUICKCHECK_MAX_INPUT_BYTES, + QUICKCHECK_NUM_CASES, +}; + +#[test] +fn test_empty_string() { + let mut hasher = Keccak256::new(); + hasher.update(&[]); + let result = hasher.finalize(); + println!("Empty string hash = {}", hex::encode(result.as_slice())); + + let mut our_hasher = Keccak256::default(); + let mut block = [0u8; 136]; + block[0] = 0x01; + block[135] = 0x80; + our_hasher.update(&block); + let state_inner = transmute_state(our_hasher); + for (idx, el) in state_inner.iter().enumerate() { + println!("Element {} = 0x{:016x}", idx, el); + } +} + +#[test] +fn legacy_backend_matches_tiny_keccak_boundary_vectors() { + for case in DETERMINISTIC_KECCAK_CASES { + assert_backend_matches_reference::(*case); + } +} + +#[test] +fn legacy_backend_matches_tiny_keccak_quickcheck() { + fn property(mut input: Vec, input_offset: u8) -> bool { + input.truncate(QUICKCHECK_MAX_INPUT_BYTES); + + let input_offset = (input_offset % 32) as u32; + let actual = execution_output_bytes(&run_keccak_precompile_test_backend::< + LegacyKeccakBackend, + >(input_offset, &input)); + let reference = reference_keccak256(&input); + + actual == reference + } + + QuickCheck::new() + .tests(QUICKCHECK_NUM_CASES) + .quickcheck(property as fn(Vec, u8) -> bool); +} + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use super::airbender_backend::DelegatedKeccakBackend; + + #[test] + fn delegated_backend_matches_tiny_keccak_boundary_vectors() { + for case in DETERMINISTIC_KECCAK_CASES { + assert_backend_matches_reference::(*case); + } + } + + #[test] + fn legacy_and_delegated_backends_match_tiny_keccak_quickcheck() { + fn property(mut input: Vec, input_offset: u8) -> bool { + input.truncate(QUICKCHECK_MAX_INPUT_BYTES); + + let input_offset = (input_offset % 32) as u32; + let reference = reference_keccak256(&input); + let legacy = execution_output_bytes( + &run_keccak_precompile_test_backend::(input_offset, &input), + ); + let delegated = execution_output_bytes( + &run_keccak_precompile_test_backend::(input_offset, &input), + ); + + legacy == reference && delegated == reference + } + + QuickCheck::new() + .tests(QUICKCHECK_NUM_CASES) + .quickcheck(property as fn(Vec, u8) -> bool); + } + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/keccak256/tests/utils.rs b/crates/zk_evm_abstractions/src/precompiles/keccak256/tests/utils.rs new file mode 100644 index 00000000..c0104fc2 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/keccak256/tests/utils.rs @@ -0,0 +1,242 @@ +use zkevm_opcode_defs::ethereum_types::{Address, U256}; +use zkevm_opcode_defs::PrecompileCallABI; + +use crate::aux::Timestamp; +use crate::queries::{LogQuery, MemoryQuery}; +use crate::vm::Memory; + +use super::super::{ + execute_keccak_precompile, Keccak256RoundWitness, KeccakBackend, KECCAK_RATE_BYTES, +}; + +pub(super) type KeccakExecution = ( + usize, + Option<( + Vec, + Vec, + Vec, + )>, +); + +pub(super) const QUICKCHECK_NUM_CASES: u64 = 256; +pub(super) const QUICKCHECK_MAX_INPUT_BYTES: usize = 2048; +const KECCAK_RATE_BOUNDARY: usize = KECCAK_RATE_BYTES; + +#[derive(Clone, Copy, Debug)] +pub(super) struct DeterministicKeccakCase { + name: &'static str, + input_length: usize, + input_offset: u32, + seed: u64, +} + +impl DeterministicKeccakCase { + fn input(self) -> Vec { + let mut state = self.seed; + + (0..self.input_length) + .map(|idx| { + // A tiny deterministic byte stream is enough here: the goal is to keep the + // message contents stable while making the long-message cases less uniform than + // the usual repeated-byte fixtures. + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + + (state as u8).wrapping_add((idx as u8).wrapping_mul(17)) + }) + .collect() + } +} + +pub(super) const DETERMINISTIC_KECCAK_CASES: &[DeterministicKeccakCase] = &[ + DeterministicKeccakCase { + name: "empty-aligned", + input_length: 0, + input_offset: 0, + seed: 0x01, + }, + DeterministicKeccakCase { + name: "crosses-word-boundary", + input_length: 33, + input_offset: 31, + seed: 0x23, + }, + DeterministicKeccakCase { + name: "rate-minus-one", + input_length: KECCAK_RATE_BOUNDARY - 1, + input_offset: 17, + seed: 0x45, + }, + DeterministicKeccakCase { + name: "rate", + input_length: KECCAK_RATE_BOUNDARY, + input_offset: 0, + seed: 0x67, + }, + DeterministicKeccakCase { + name: "rate-plus-one", + input_length: KECCAK_RATE_BOUNDARY + 1, + input_offset: 1, + seed: 0x89, + }, + DeterministicKeccakCase { + name: "two-rates-minus-one", + input_length: (2 * KECCAK_RATE_BOUNDARY) - 1, + input_offset: 5, + seed: 0xab, + }, + DeterministicKeccakCase { + name: "two-rates", + input_length: 2 * KECCAK_RATE_BOUNDARY, + input_offset: 31, + seed: 0xcd, + }, + DeterministicKeccakCase { + name: "long-512", + input_length: 512, + input_offset: 7, + seed: 0x101, + }, + DeterministicKeccakCase { + name: "long-1024", + input_length: 1024, + input_offset: 13, + seed: 0x202, + }, + DeterministicKeccakCase { + name: "long-1536", + input_length: 1536, + input_offset: 29, + seed: 0x303, + }, +]; + +#[derive(Debug, Default)] +struct TestInputMemory { + words: Vec, +} + +impl TestInputMemory { + fn for_input(input_offset: u32, input: &[u8]) -> Self { + let total_bytes = input_offset as usize + input.len(); + let num_words = (total_bytes + 31) / 32; + let mut bytes = vec![0u8; num_words * 32]; + let input_offset = input_offset as usize; + + bytes[input_offset..(input_offset + input.len())].copy_from_slice(input); + + let words = bytes + .chunks(32) + .map(U256::from_big_endian) + .collect::>(); + + Self { words } + } +} + +impl Memory for TestInputMemory { + fn execute_partial_query( + &mut self, + _monotonic_cycle_counter: u32, + mut query: MemoryQuery, + ) -> MemoryQuery { + if !query.rw_flag { + query.value = self + .words + .get(query.location.index.0 as usize) + .copied() + .unwrap_or_else(U256::zero); + } + + query + } + + fn specialized_code_query( + &mut self, + _monotonic_cycle_counter: u32, + _query: MemoryQuery, + ) -> MemoryQuery { + unreachable!("keccak precompile does not issue code queries") + } + + fn read_code_query(&self, _monotonic_cycle_counter: u32, _query: MemoryQuery) -> MemoryQuery { + unreachable!("keccak precompile does not issue code queries") + } +} + +fn make_keccak_test_query(input_offset: u32, input_length: u32) -> LogQuery { + let abi = PrecompileCallABI { + input_memory_offset: input_offset, + input_memory_length: input_length, + output_memory_offset: 0, + output_memory_length: 1, + memory_page_to_read: 1, + memory_page_to_write: 2, + precompile_interpreted_data: 0, + }; + + LogQuery { + timestamp: Timestamp(1), + tx_number_in_block: 0, + aux_byte: 0, + shard_id: 0, + address: Address::zero(), + key: abi.to_u256(), + read_value: U256::zero(), + written_value: U256::zero(), + rw_flag: false, + rollback: false, + is_service: false, + } +} + +pub(super) fn run_keccak_precompile_test_backend( + input_offset: u32, + input: &[u8], +) -> KeccakExecution { + let query = make_keccak_test_query(input_offset, input.len() as u32); + let mut memory = TestInputMemory::for_input(input_offset, input); + execute_keccak_precompile::(0, query, &mut memory) +} + +pub(super) fn execution_output_bytes(execution: &KeccakExecution) -> [u8; 32] { + let (_, witness) = execution; + let (_, write_queries, _) = witness + .as_ref() + .expect("keccak test execution must produce witness"); + let result_query = write_queries + .last() + .expect("keccak test execution must include one write"); + + let mut output = [0u8; 32]; + result_query.value.to_big_endian(&mut output); + output +} + +pub(super) fn reference_keccak256(input: &[u8]) -> [u8; 32] { + use tiny_keccak::{Hasher, Keccak as TinyKeccak}; + + let mut state = TinyKeccak::v256(); + let mut output = [0u8; 32]; + state.update(input); + state.finalize(&mut output); + output +} + +pub(super) fn assert_backend_matches_reference( + case: DeterministicKeccakCase, +) { + let input = case.input(); + let actual = execution_output_bytes(&run_keccak_precompile_test_backend::( + case.input_offset, + &input, + )); + let reference = reference_keccak256(&input); + + assert_eq!( + actual, reference, + "backend must match tiny-keccak for case='{}', offset={}, length={}", + case.name, case.input_offset, case.input_length, + ); +} diff --git a/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify.rs b/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify.rs deleted file mode 100644 index ece73a92..00000000 --- a/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify.rs +++ /dev/null @@ -1,333 +0,0 @@ -use zkevm_opcode_defs::{ethereum_types::U256, p256}; - -use super::*; - -// we need hash, r, s, x, y -pub const MEMORY_READS_PER_CYCLE: usize = 5; -pub const MEMORY_WRITES_PER_CYCLE: usize = 2; - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Secp256r1VerifyRoundWitness { - pub new_request: LogQuery, - pub reads: [MemoryQuery; MEMORY_READS_PER_CYCLE], - pub writes: [MemoryQuery; MEMORY_WRITES_PER_CYCLE], -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Secp256r1VerifyPrecompile; - -impl Precompile for Secp256r1VerifyPrecompile { - type CycleWitness = Secp256r1VerifyRoundWitness; - - fn execute_precompile( - &mut self, - monotonic_cycle_counter: u32, - query: LogQuery, - memory: &mut M, - ) -> ( - usize, - Option<(Vec, Vec, Vec)>, - ) { - const NUM_ROUNDS: usize = 1; - - // read the parameters - let precompile_call_params = query; - let params = precompile_abi_in_log(precompile_call_params); - let timestamp_to_read = precompile_call_params.timestamp; - let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); // our default timestamping agreement - - let mut current_read_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_read), - index: MemoryIndex(params.input_memory_offset), - }; - - // we assume that we have - // - hash of the message - // - r - // - s - // - x - // - y - - // NOTE: we assume system contract to do pre-checks, but anyway catch cases of invalid ranges or point - // not on curve here - - let mut read_history = if B { - Vec::with_capacity(MEMORY_READS_PER_CYCLE) - } else { - vec![] - }; - let mut write_history = if B { - Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) - } else { - vec![] - }; - - let mut round_witness = Secp256r1VerifyRoundWitness { - new_request: precompile_call_params, - reads: [MemoryQuery::empty(); MEMORY_READS_PER_CYCLE], - writes: [MemoryQuery::empty(); MEMORY_WRITES_PER_CYCLE], - }; - - let mut read_idx = 0; - - let hash_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let hash_query = memory.execute_partial_query(monotonic_cycle_counter, hash_query); - let hash_value = hash_query.value; - if B { - round_witness.reads[read_idx] = hash_query; - read_idx += 1; - read_history.push(hash_query); - } - - current_read_location.index.0 += 1; - let r_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let r_query = memory.execute_partial_query(monotonic_cycle_counter, r_query); - let r_value = r_query.value; - if B { - round_witness.reads[read_idx] = r_query; - read_idx += 1; - read_history.push(r_query); - } - - current_read_location.index.0 += 1; - let s_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let s_query = memory.execute_partial_query(monotonic_cycle_counter, s_query); - let s_value = s_query.value; - if B { - round_witness.reads[read_idx] = s_query; - read_idx += 1; - read_history.push(s_query); - } - - current_read_location.index.0 += 1; - let x_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let x_query = memory.execute_partial_query(monotonic_cycle_counter, x_query); - let x_value = x_query.value; - if B { - round_witness.reads[read_idx] = x_query; - read_idx += 1; - read_history.push(x_query); - } - - current_read_location.index.0 += 1; - let y_query = MemoryQuery { - timestamp: timestamp_to_read, - location: current_read_location, - value: U256::zero(), - value_is_pointer: false, - rw_flag: false, - }; - let y_query = memory.execute_partial_query(monotonic_cycle_counter, y_query); - let y_value = y_query.value; - if B { - round_witness.reads[read_idx] = y_query; - // read_idx += 1; - read_history.push(y_query); - } - - // read everything as bytes for ecrecover purposes - - let mut buffer = [0u8; 32]; - hash_value.to_big_endian(&mut buffer[..]); - let hash = buffer; - - r_value.to_big_endian(&mut buffer[..]); - let r_bytes = buffer; - - s_value.to_big_endian(&mut buffer[..]); - let s_bytes = buffer; - - x_value.to_big_endian(&mut buffer[..]); - let x_bytes = buffer; - - y_value.to_big_endian(&mut buffer[..]); - let y_bytes = buffer; - - let result = secp256r1_verify_inner(&hash, &r_bytes, &s_bytes, &x_bytes, &y_bytes); - - if let Ok(is_valid) = result { - let mut write_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_write), - index: MemoryIndex(params.output_memory_offset), - }; - - let ok_marker = U256::one(); - let ok_or_err_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: ok_marker, - value_is_pointer: false, - rw_flag: true, - }; - let ok_or_err_query = - memory.execute_partial_query(monotonic_cycle_counter, ok_or_err_query); - - write_location.index.0 += 1; - let result = U256::from(is_valid as u64); - let result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: result, - value_is_pointer: false, - rw_flag: true, - }; - let result_query = memory.execute_partial_query(monotonic_cycle_counter, result_query); - - if B { - round_witness.writes[0] = ok_or_err_query; - round_witness.writes[1] = result_query; - write_history.push(ok_or_err_query); - write_history.push(result_query); - } - } else { - let mut write_location = MemoryLocation { - memory_type: MemoryType::Heap, // we default for some value, here it's not that important - page: MemoryPage(params.memory_page_to_write), - index: MemoryIndex(params.output_memory_offset), - }; - - let err_marker = U256::zero(); - let ok_or_err_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: err_marker, - value_is_pointer: false, - rw_flag: true, - }; - let ok_or_err_query = - memory.execute_partial_query(monotonic_cycle_counter, ok_or_err_query); - - write_location.index.0 += 1; - let empty_result = U256::zero(); - let result_query = MemoryQuery { - timestamp: timestamp_to_write, - location: write_location, - value: empty_result, - value_is_pointer: false, - rw_flag: true, - }; - let result_query = memory.execute_partial_query(monotonic_cycle_counter, result_query); - - if B { - round_witness.writes[0] = ok_or_err_query; - round_witness.writes[1] = result_query; - write_history.push(ok_or_err_query); - write_history.push(result_query); - } - } - - let witness = if B { - Some((read_history, write_history, vec![round_witness])) - } else { - None - }; - - (NUM_ROUNDS, witness) - } -} - -pub fn secp256r1_verify_inner( - digest: &[u8; 32], - r: &[u8; 32], - s: &[u8; 32], - x: &[u8; 32], - y: &[u8; 32], -) -> Result { - use p256::ecdsa::signature::hazmat::PrehashVerifier; - use p256::ecdsa::{Signature, VerifyingKey}; - use p256::elliptic_curve::generic_array::GenericArray; - use p256::elliptic_curve::sec1::FromEncodedPoint; - use p256::{AffinePoint, EncodedPoint}; - - // we expect pre-validation, so this check always works - let signature = Signature::from_scalars( - GenericArray::clone_from_slice(r), - GenericArray::clone_from_slice(s), - ) - .map_err(|_| ())?; - - let encoded_pk = EncodedPoint::from_affine_coordinates( - &GenericArray::clone_from_slice(x), - &GenericArray::clone_from_slice(y), - false, - ); - - let may_be_pk_point = AffinePoint::from_encoded_point(&encoded_pk); - if bool::from(may_be_pk_point.is_none()) { - return Err(()); - } - let pk_point = may_be_pk_point.unwrap(); - - let verifier = VerifyingKey::from_affine(pk_point).map_err(|_| ())?; - - let result = verifier.verify_prehash(digest, &signature); - - Ok(result.is_ok()) -} - -pub fn secp256r1_verify_function( - monotonic_cycle_counter: u32, - precompile_call_params: LogQuery, - memory: &mut M, -) -> ( - usize, - Option<( - Vec, - Vec, - Vec, - )>, -) { - let mut processor = Secp256r1VerifyPrecompile::; - processor.execute_precompile(monotonic_cycle_counter, precompile_call_params, memory) -} - -#[cfg(test)] -mod tests { - use super::secp256r1_verify_inner; - - fn hex_to_32(hex_str: &str) -> [u8; 32] { - let bytes = hex::decode(hex_str).expect("hex decode should succeed"); - bytes - .as_slice() - .try_into() - .expect("hex string must be 32 bytes") - } - - #[test] - fn secp256r1_r_at_infinity_vector() { - let digest = hex_to_32("0000000000000000000000000000000000000000000000000000000000000001"); - let r = hex_to_32("0000000000000000000000000000000000000000000000000000000000000001"); - let s = hex_to_32("0000000000000000000000000000000000000000000000000000000000000001"); - let pk_x = hex_to_32("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296"); - let pk_y = hex_to_32("b01cbd1c01e58065711814b583f061e9d431cca994cea1313449bf97c840ae0a"); - - let result = secp256r1_verify_inner(&digest, &r, &s, &pk_x, &pk_y); - assert_eq!(result.unwrap(), false); - } -} diff --git a/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/airbender_backend.rs b/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/airbender_backend.rs new file mode 100644 index 00000000..b95f521a --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/airbender_backend.rs @@ -0,0 +1,53 @@ +use super::Secp256r1Backend; +use zkevm_opcode_defs::p256; + +// ============================================================================== +// Delegated Backend +// ============================================================================== +pub(super) struct DelegatedSecp256r1Backend; + +impl Secp256r1Backend for DelegatedSecp256r1Backend { + fn verify( + digest: &[u8; 32], + r: &[u8; 32], + s: &[u8; 32], + x: &[u8; 32], + y: &[u8; 32], + ) -> Result { + validate_public_key(x, y)?; + + match airbender_crypto::secp256r1::verify(digest, r, s, x, y) { + Ok(is_valid) => Ok(is_valid), + // A recovered point at infinity is an invalid ECDSA signature, not + // malformed precompile input. Legacy completes verification and + // returns `false`, which writes the successful-but-invalid output + // word pair `[1, 0]`. + Err(airbender_crypto::secp256r1::Secp256r1Err::RecoveredInfinity) => Ok(false), + Err(_) => Err(()), + } + } +} + +fn validate_public_key(x: &[u8; 32], y: &[u8; 32]) -> Result<(), ()> { + use p256::elliptic_curve::generic_array::GenericArray; + use p256::elliptic_curve::sec1::FromEncodedPoint; + use p256::{AffinePoint, EncodedPoint}; + + // Legacy rejects malformed public keys before verification. Keeping the + // same parse boundary lets the delegated backend reserve + // `RecoveredInfinity` for the verification result instead of conflating it + // with invalid key coordinates. + let encoded_key = EncodedPoint::from_affine_coordinates( + &GenericArray::clone_from_slice(x), + &GenericArray::clone_from_slice(y), + false, + ); + let public_key_point = AffinePoint::from_encoded_point(&encoded_key); + + // `public_key_point.is_none()` returns `Choice`, not `bool` + if bool::from(public_key_point.is_none()) { + Err(()) + } else { + Ok(()) + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/legacy_backend.rs b/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/legacy_backend.rs new file mode 100644 index 00000000..4f830358 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/legacy_backend.rs @@ -0,0 +1,44 @@ +use zkevm_opcode_defs::p256; + +use super::Secp256r1Backend; + +// ============================================================================== +// Legacy Backend +// ============================================================================== +pub(super) struct LegacySecp256r1Backend; + +impl Secp256r1Backend for LegacySecp256r1Backend { + fn verify( + digest: &[u8; 32], + r: &[u8; 32], + s: &[u8; 32], + x: &[u8; 32], + y: &[u8; 32], + ) -> Result { + use p256::ecdsa::signature::hazmat::PrehashVerifier; + use p256::ecdsa::{Signature, VerifyingKey}; + use p256::elliptic_curve::generic_array::GenericArray; + use p256::elliptic_curve::sec1::FromEncodedPoint; + use p256::{AffinePoint, EncodedPoint}; + + let signature = Signature::from_scalars( + GenericArray::clone_from_slice(r), + GenericArray::clone_from_slice(s), + ) + .map_err(|_| ())?; + + let encoded_key = EncodedPoint::from_affine_coordinates( + &GenericArray::clone_from_slice(x), + &GenericArray::clone_from_slice(y), + false, + ); + + let public_key_point = AffinePoint::from_encoded_point(&encoded_key); + if bool::from(public_key_point.is_none()) { + return Err(()); + } + + let verifying_key = VerifyingKey::from_affine(public_key_point.unwrap()).map_err(|_| ())?; + Ok(verifying_key.verify_prehash(digest, &signature).is_ok()) + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/mod.rs b/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/mod.rs new file mode 100644 index 00000000..95868d42 --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/mod.rs @@ -0,0 +1,195 @@ +use cfg_if::cfg_if; +use zkevm_opcode_defs::ethereum_types::U256; + +use super::*; + +#[cfg(feature = "airbender-precompile-delegations")] +mod airbender_backend; +#[cfg(any(not(feature = "airbender-precompile-delegations"), test))] +mod legacy_backend; +#[cfg(test)] +mod tests; + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use self::airbender_backend::DelegatedSecp256r1Backend as ActiveSecp256r1Backend; + } else { + use self::legacy_backend::LegacySecp256r1Backend as ActiveSecp256r1Backend; + } +} + +// We need hash, r, s, x, y. +pub const MEMORY_READS_PER_CYCLE: usize = 5; +pub const MEMORY_WRITES_PER_CYCLE: usize = 2; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Secp256r1VerifyRoundWitness { + pub new_request: LogQuery, + pub reads: [MemoryQuery; MEMORY_READS_PER_CYCLE], + pub writes: [MemoryQuery; MEMORY_WRITES_PER_CYCLE], +} + +trait Secp256r1Backend { + fn verify( + digest: &[u8; 32], + r: &[u8; 32], + s: &[u8; 32], + x: &[u8; 32], + y: &[u8; 32], + ) -> Result; +} + +fn execute_secp256r1_precompile( + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<( + Vec, + Vec, + Vec, + )>, +) { + const NUM_ROUNDS: usize = 1; + + let precompile_call_params = query; + let params = precompile_abi_in_log(precompile_call_params); + let timestamp_to_read = precompile_call_params.timestamp; + let timestamp_to_write = Timestamp(timestamp_to_read.0 + 1); + + let mut current_read_location = MemoryLocation { + memory_type: MemoryType::Heap, + page: MemoryPage(params.memory_page_to_read), + index: MemoryIndex(params.input_memory_offset), + }; + + let mut read_history = if B { + Vec::with_capacity(MEMORY_READS_PER_CYCLE) + } else { + vec![] + }; + let mut write_history = if B { + Vec::with_capacity(MEMORY_WRITES_PER_CYCLE) + } else { + vec![] + }; + + let mut round_witness = Secp256r1VerifyRoundWitness { + new_request: precompile_call_params, + reads: [MemoryQuery::empty(); MEMORY_READS_PER_CYCLE], + writes: [MemoryQuery::empty(); MEMORY_WRITES_PER_CYCLE], + }; + + let mut input_words = [U256::zero(); MEMORY_READS_PER_CYCLE]; + for (idx, input_word) in input_words.iter_mut().enumerate() { + let read_query = MemoryQuery { + timestamp: timestamp_to_read, + location: current_read_location, + value: U256::zero(), + value_is_pointer: false, + rw_flag: false, + }; + let read_query = memory.execute_partial_query(monotonic_cycle_counter, read_query); + *input_word = read_query.value; + + if B { + round_witness.reads[idx] = read_query; + read_history.push(read_query); + } + + current_read_location.index.0 += 1; + } + + let digest = u256_to_bytes32(input_words[0]); + let r = u256_to_bytes32(input_words[1]); + let s = u256_to_bytes32(input_words[2]); + let x = u256_to_bytes32(input_words[3]); + let y = u256_to_bytes32(input_words[4]); + + let result = Backend::verify(&digest, &r, &s, &x, &y); + let output_values = match result { + Ok(is_valid) => [U256::one(), U256::from(is_valid as u64)], + Err(_) => [U256::zero(), U256::zero()], + }; + + let mut write_location = MemoryLocation { + memory_type: MemoryType::Heap, + page: MemoryPage(params.memory_page_to_write), + index: MemoryIndex(params.output_memory_offset), + }; + + for (idx, value) in output_values.into_iter().enumerate() { + let write_query = MemoryQuery { + timestamp: timestamp_to_write, + location: write_location, + value, + value_is_pointer: false, + rw_flag: true, + }; + let write_query = memory.execute_partial_query(monotonic_cycle_counter, write_query); + + if B { + round_witness.writes[idx] = write_query; + write_history.push(write_query); + } + + write_location.index.0 += 1; + } + + let witness = if B { + Some((read_history, write_history, vec![round_witness])) + } else { + None + }; + + (NUM_ROUNDS, witness) +} + +fn u256_to_bytes32(value: U256) -> [u8; 32] { + let mut bytes = [0u8; 32]; + value.to_big_endian(&mut bytes); + bytes +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Secp256r1VerifyPrecompile; + +impl Precompile for Secp256r1VerifyPrecompile { + type CycleWitness = Secp256r1VerifyRoundWitness; + + fn execute_precompile( + &mut self, + monotonic_cycle_counter: u32, + query: LogQuery, + memory: &mut M, + ) -> ( + usize, + Option<(Vec, Vec, Vec)>, + ) { + execute_secp256r1_precompile::( + monotonic_cycle_counter, + query, + memory, + ) + } +} + +pub fn secp256r1_verify_function( + monotonic_cycle_counter: u32, + precompile_call_params: LogQuery, + memory: &mut M, +) -> ( + usize, + Option<( + Vec, + Vec, + Vec, + )>, +) { + execute_secp256r1_precompile::( + monotonic_cycle_counter, + precompile_call_params, + memory, + ) +} diff --git a/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/tests/mod.rs b/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/tests/mod.rs new file mode 100644 index 00000000..662e284a --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/tests/mod.rs @@ -0,0 +1,72 @@ +use cfg_if::cfg_if; +use quickcheck::QuickCheck; + +use super::legacy_backend::LegacySecp256r1Backend; + +mod utils; + +use self::utils::{ + assert_backend_matches_case, deterministic_secp256r1_cases, + legacy_backend_matches_signed_message, wycheproof_edge_case_divergences, + QUICKCHECK_MAX_MESSAGE_BYTES, QUICKCHECK_NUM_CASES, +}; + +#[test] +fn legacy_backend_matches_static_vectors() { + for case in deterministic_secp256r1_cases() { + assert_backend_matches_case::(&case); + } +} + +#[test] +fn legacy_backend_treats_wycheproof_edge_cases_as_failed_verification() { + for case in wycheproof_edge_case_divergences() { + assert_backend_matches_case::(&case); + } +} + +#[test] +fn legacy_backend_matches_signing_reference_quickcheck() { + fn property(mut message: Vec) -> bool { + message.truncate(QUICKCHECK_MAX_MESSAGE_BYTES); + legacy_backend_matches_signed_message::(&message) + } + + QuickCheck::new() + .tests(QUICKCHECK_NUM_CASES) + .quickcheck(property as fn(Vec) -> bool); +} + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + use super::airbender_backend::DelegatedSecp256r1Backend; + use self::utils::delegated_backend_matches_signed_message; + + #[test] + fn delegated_backend_matches_static_vectors() { + for case in deterministic_secp256r1_cases() { + assert_backend_matches_case::(&case); + } + } + + #[test] + fn delegated_backend_matches_legacy_on_wycheproof_edge_cases() { + for case in wycheproof_edge_case_divergences() { + assert_backend_matches_case::(&case); + } + } + + #[test] + fn delegated_backend_matches_legacy_quickcheck() { + fn property(mut message: Vec) -> bool { + message.truncate(QUICKCHECK_MAX_MESSAGE_BYTES); + legacy_backend_matches_signed_message::(&message) + && delegated_backend_matches_signed_message::(&message) + } + + QuickCheck::new() + .tests(QUICKCHECK_NUM_CASES) + .quickcheck(property as fn(Vec) -> bool); + } + } +} diff --git a/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/tests/utils.rs b/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/tests/utils.rs new file mode 100644 index 00000000..09a72eaa --- /dev/null +++ b/crates/zk_evm_abstractions/src/precompiles/secp256r1_verify/tests/utils.rs @@ -0,0 +1,224 @@ +use cfg_if::cfg_if; +use zkevm_opcode_defs::p256; +use zkevm_opcode_defs::sha2::Digest; +use zkevm_opcode_defs::sha3; + +use super::super::Secp256r1Backend; + +pub(super) const QUICKCHECK_NUM_CASES: u64 = 256; +pub(super) const QUICKCHECK_MAX_MESSAGE_BYTES: usize = 256; + +#[derive(Clone, Copy)] +pub(super) struct DeterministicSecp256r1Case { + name: &'static str, + digest: [u8; 32], + r: [u8; 32], + s: [u8; 32], + x: [u8; 32], + y: [u8; 32], + expected: Result, +} + +// These vectors are structurally valid secp256r1 inputs where verification +// reaches an exceptional elliptic-curve state. The precompile contract +// distinguishes malformed inputs (`Err(())`) from well-formed signatures that +// simply do not verify (`Ok(false)`), so these must stay on the latter path. +// +// Attribution: These test cases are taken from the wycheproof project, licensed +// under Apache 2.0 license. +// Project repository: https://github.com/C2SP/wycheproof/ +pub(super) fn wycheproof_edge_case_divergences() -> Vec { + vec![ + DeterministicSecp256r1Case { + name: "wycheproof-169-point-at-infinity-during-verify", + digest: hex_to_32("bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023"), + r: hex_to_32("7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8"), + s: hex_to_32("555555550000000055555555555555553ef7a8e48d07df81a693439654210c70"), + x: hex_to_32("b533d4695dd5b8c5e07757e55e6e516f7e2c88fa0239e23f60e8ec07dd70f287"), + y: hex_to_32("1b134ee58cc583278456863f33c3a85d881f7d4a39850143e29d4eaf009afe47"), + expected: Ok(false), + }, + DeterministicSecp256r1Case { + name: "wycheproof-205-duplication-bug", + digest: hex_to_32("bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023"), + r: hex_to_32("6f2347cab7dd76858fe0555ac3bc99048c4aacafdfb6bcbe05ea6c42c4934569"), + s: hex_to_32("bb726660235793aa9957a61e76e00c2c435109cf9a15dd624d53f4301047856b"), + x: hex_to_32("5b812fd521aafa69835a849cce6fbdeb6983b442d2444fe70e134c027fc46963"), + y: hex_to_32("7c75bf0c5c9f6d17ffb16d2726bf30a9c7aaf31a8d317472b1ea145ab66db616"), + expected: Ok(false), + }, + DeterministicSecp256r1Case { + name: "wycheproof-208-comparison-with-point-at-infinity", + digest: hex_to_32("bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023"), + r: hex_to_32("555555550000000055555555555555553ef7a8e48d07df81a693439654210c70"), + s: hex_to_32("3333333300000000333333333333333325c7cbbc549e52e763f1f55a327a3aa9"), + x: hex_to_32("dd86d3b5f4a13e8511083b78002081c53ff467f11ebd98a51a633db76665d250"), + y: hex_to_32("45d5c8200c89f2fa10d849349226d21d8dfaed6ff8d5cb3e1b7e17474ebc18f7"), + expected: Ok(false), + }, + DeterministicSecp256r1Case { + name: "wycheproof-222-public-key-shares-generator-x-low-y", + digest: hex_to_32("bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023"), + r: hex_to_32("44a5ad0ad0636d9f12bc9e0a6bdd5e1cbcb012ea7bf091fcec15b0c43202d52e"), + s: hex_to_32("249249246db6db6ddb6db6db6db6db6dad4591868595a8ee6bf5f864ff7be0c2"), + x: hex_to_32("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296"), + y: hex_to_32("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5"), + expected: Ok(false), + }, + DeterministicSecp256r1Case { + name: "wycheproof-223-public-key-shares-generator-x-high-y", + digest: hex_to_32("bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023"), + r: hex_to_32("bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023"), + s: hex_to_32("249249246db6db6ddb6db6db6db6db6dad4591868595a8ee6bf5f864ff7be0c2"), + x: hex_to_32("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296"), + y: hex_to_32("b01cbd1c01e58065711814b583f061e9d431cca994cea1313449bf97c840ae0a"), + expected: Ok(false), + }, + ] +} + +pub(super) fn deterministic_secp256r1_cases() -> Vec { + let valid_case = signed_case( + "valid-signature", + hex_to_32("8854b52e0d56cb713f1189b15fd3684670e8c89ce11b7bcff37204d894f2519a"), + hex_to_32("0101010101010101010101010101010101010101010101010101010101010101"), + ); + + let mut bad_digest = valid_case.digest; + bad_digest[0] ^= 1; + let mut bad_r = valid_case.r; + bad_r[31] ^= 1; + + vec![ + valid_case, + DeterministicSecp256r1Case { + name: "tampered-digest", + digest: bad_digest, + r: valid_case.r, + s: valid_case.s, + x: valid_case.x, + y: valid_case.y, + expected: Ok(false), + }, + DeterministicSecp256r1Case { + name: "tampered-signature", + digest: valid_case.digest, + r: bad_r, + s: valid_case.s, + x: valid_case.x, + y: valid_case.y, + expected: Ok(false), + }, + DeterministicSecp256r1Case { + name: "invalid-zero-coordinates", + digest: hex_to_32("0000000000000000000000000000000000000000000000000000000000000001"), + r: hex_to_32("0000000000000000000000000000000000000000000000000000000000000001"), + s: hex_to_32("0000000000000000000000000000000000000000000000000000000000000001"), + x: [0u8; 32], + y: [0u8; 32], + expected: Err(()), + }, + ] +} + +pub(super) fn assert_backend_matches_case( + case: &DeterministicSecp256r1Case, +) { + let actual = Backend::verify(&case.digest, &case.r, &case.s, &case.x, &case.y); + match (&case.expected, actual) { + (Ok(expected), Ok(actual)) => assert_eq!( + actual, *expected, + "backend must match static vector '{}'", + case.name, + ), + (Err(_), Err(_)) => {} + (Ok(_), Err(_)) => panic!("backend unexpectedly failed for vector '{}'", case.name), + (Err(_), Ok(actual)) => panic!( + "backend unexpectedly succeeded for vector '{}': {actual}", + case.name, + ), + } +} + +pub(super) fn legacy_backend_matches_signed_message( + message: &[u8], +) -> bool { + let digest = sha3::Keccak256::digest(message); + let mut digest_bytes = [0u8; 32]; + digest_bytes.copy_from_slice(digest.as_slice()); + backend_matches_case::(&signed_case( + "quickcheck-message", + hex_to_32("49a3f7e1d4c6b8a2908172635445362718190a0b0c0d0e0f1021324354657687"), + digest_bytes, + )) +} + +cfg_if! { + if #[cfg(feature = "airbender-precompile-delegations")] { + pub(super) fn delegated_backend_matches_signed_message( + message: &[u8], + ) -> bool { + let digest = sha3::Keccak256::digest(message); + let mut digest_bytes = [0u8; 32]; + digest_bytes.copy_from_slice(digest.as_slice()); + backend_matches_case::(&signed_case( + "quickcheck-message", + hex_to_32("8854b52e0d56cb713f1189b15fd3684670e8c89ce11b7bcff37204d894f2519a"), + digest_bytes, + )) + } + } +} + +fn backend_matches_case(case: &DeterministicSecp256r1Case) -> bool { + match ( + &case.expected, + Backend::verify(&case.digest, &case.r, &case.s, &case.x, &case.y), + ) { + (Ok(expected), Ok(actual)) => *expected == actual, + (Err(_), Err(_)) => true, + _ => false, + } +} + +fn signed_case( + name: &'static str, + private_key: [u8; 32], + digest: [u8; 32], +) -> DeterministicSecp256r1Case { + use p256::ecdsa::signature::hazmat::PrehashSigner; + use p256::ecdsa::{Signature, SigningKey}; + + let signing_key = + SigningKey::from_bytes((&private_key).into()).expect("private key vector must be valid"); + let verifying_key = signing_key.verifying_key(); + let encoded = verifying_key.to_encoded_point(false); + let (x, y) = match encoded.coordinates() { + p256::elliptic_curve::sec1::Coordinates::Uncompressed { x, y } => (*x, *y), + _ => panic!("signing key must produce uncompressed coordinates"), + }; + let x: [u8; 32] = x.into(); + let y: [u8; 32] = y.into(); + + let signature: Signature = signing_key + .sign_prehash(&digest) + .expect("prehash signing must succeed"); + + DeterministicSecp256r1Case { + name, + digest, + r: signature.r().to_bytes().into(), + s: signature.s().to_bytes().into(), + x, + y, + expected: Ok(true), + } +} + +fn hex_to_32(hex_str: &str) -> [u8; 32] { + let bytes = hex::decode(hex_str).expect("hex decode should succeed"); + bytes + .as_slice() + .try_into() + .expect("hex string must be 32 bytes") +} diff --git a/crates/zk_evm_abstractions/src/utils/airbender_bn254.rs b/crates/zk_evm_abstractions/src/utils/airbender_bn254.rs new file mode 100644 index 00000000..c6882a3b --- /dev/null +++ b/crates/zk_evm_abstractions/src/utils/airbender_bn254.rs @@ -0,0 +1,74 @@ +use std::str::FromStr; + +use airbender_crypto::ark_ec::AffineRepr; +use airbender_crypto::ark_ff::{BigInteger as AirBigInteger, PrimeField as AirPrimeField}; +use airbender_crypto::bn254::{Fq as AirFq, Fr as AirFr, G1Affine as AirG1Affine}; +use anyhow::{Error, Result}; +use zkevm_opcode_defs::ethereum_types::U256; + +use crate::utils::bn254::ECPointCoordinates; + +pub(crate) fn airbender_field_element_to_u256(value: F) -> U256 { + let bytes = value.into_bigint().to_bytes_be(); + let mut padded = [0u8; 32]; + let write_offset = padded + .len() + .checked_sub(bytes.len()) + .expect("field element byte representation must not exceed 32 bytes"); + padded[write_offset..].copy_from_slice(&bytes); + U256::from_big_endian(&padded) +} + +pub(crate) fn airbender_point_to_u256_tuple(point: AirG1Affine) -> ECPointCoordinates { + if point.is_zero() { + return (U256::zero(), U256::zero()); + } + + let (x, y) = point + .xy() + .expect("non-infinity BN254 G1 point must expose coordinates"); + ( + airbender_field_element_to_u256(x), + airbender_field_element_to_u256(y), + ) +} + +pub(crate) fn airbender_fq_from_u256(value: U256, err: &'static str) -> Result { + AirFq::from_str(value.to_string().as_str()).map_err(|_| Error::msg(err)) +} + +pub(crate) fn airbender_g1_from_coordinates( + (x, y): ECPointCoordinates, + invalid_x_err: &'static str, + invalid_y_err: &'static str, +) -> Result { + if x.is_zero() && y.is_zero() { + return Ok(AirG1Affine::zero()); + } + + let x = airbender_fq_from_u256(x, invalid_x_err)?; + let y = airbender_fq_from_u256(y, invalid_y_err)?; + let point = AirG1Affine::new_unchecked(x, y); + + if !point.is_on_curve() { + return Err(Error::msg("point is not on curve")); + } + if !point.is_in_correct_subgroup_assuming_on_curve() { + return Err(Error::msg("point is not in subgroup")); + } + + Ok(point) +} + +pub(crate) fn airbender_fr_from_u256( + mut scalar: U256, + group_order_hex: &str, + invalid_scalar_err: &'static str, +) -> Result { + let group_order = U256::from_str(group_order_hex).expect("group order constant must parse"); + while scalar >= group_order { + scalar -= group_order; + } + + AirFr::from_str(scalar.to_string().as_str()).map_err(|_| Error::msg(invalid_scalar_err)) +} diff --git a/crates/zk_evm_abstractions/src/utils/mod.rs b/crates/zk_evm_abstractions/src/utils/mod.rs index ee26b6d2..eaa6892f 100644 --- a/crates/zk_evm_abstractions/src/utils/mod.rs +++ b/crates/zk_evm_abstractions/src/utils/mod.rs @@ -1 +1,4 @@ pub mod bn254; + +#[cfg(feature = "airbender-precompile-delegations")] +pub(crate) mod airbender_bn254;