From f13213155f180b7c0292e5c0f395307074a5c81a Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Fri, 19 Jun 2026 12:53:50 -0300 Subject: [PATCH 1/5] Inject LambdaVM crypto into the ethrex guest --- crypto/ethrex-crypto/Cargo.lock | 1071 +++++++++++++++++++++ crypto/ethrex-crypto/Cargo.toml | 31 + crypto/ethrex-crypto/src/lib.rs | 434 +++++++++ executor/programs/rust/ethrex/Cargo.lock | 10 + executor/programs/rust/ethrex/Cargo.toml | 12 +- executor/programs/rust/ethrex/src/main.rs | 21 +- 6 files changed, 1564 insertions(+), 15 deletions(-) create mode 100644 crypto/ethrex-crypto/Cargo.lock create mode 100644 crypto/ethrex-crypto/Cargo.toml create mode 100644 crypto/ethrex-crypto/src/lib.rs diff --git a/crypto/ethrex-crypto/Cargo.lock b/crypto/ethrex-crypto/Cargo.lock new file mode 100644 index 000000000..be8a46c1e --- /dev/null +++ b/crypto/ethrex-crypto/Cargo.lock @@ -0,0 +1,1071 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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", + "itertools", + "num-bigint", + "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", + "digest", + "educe", + "itertools", + "num-bigint", + "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", + "syn 2.0.118", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[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", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "git+https://github.com/lambdaclass/bls12_381?branch=expose-affine-constructors#78cad0378b17fc3157b83f514be192bf46edf9a1" +dependencies = [ + "digest", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-default" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f2de9133f68db0d4627ad69db767726c99ff8585272716708227008d3f1bddd" +dependencies = [ + "const-default", + "critical-section", + "linked_list_allocator", + "rlsf", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ethereum-types" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab15ed80916029f878e0267c3a9f92b67df55e79af370bf66199059ae2b4ee3" +dependencies = [ + "fixed-hash", + "primitive-types", + "uint", +] + +[[package]] +name = "ethrex-crypto" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff", + "bls12_381", + "ethereum-types", + "ff", + "hex-literal", + "k256", + "num-bigint", + "p256", + "ripemd", + "sha2", + "thiserror 2.0.18", + "tiny-keccak", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "lambda-vm-ethrex-crypto" +version = "0.1.0" +dependencies = [ + "ethrex-crypto", + "k256", + "lambda-vm-syscalls", +] + +[[package]] +name = "lambda-vm-syscalls" +version = "0.1.0" +dependencies = [ + "embedded-alloc", + "getrandom 0.2.17", + "getrandom 0.3.4", + "lazy_static", + "rand 0.9.4", + "riscv", + "thiserror 1.0.69", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linked_list_allocator" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "uint", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + +[[package]] +name = "riscv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05cfa3f7b30c84536a9025150d44d26b8e1cc20ddf436448d74cd9591eefb25" +dependencies = [ + "critical-section", + "embedded-hal", + "paste", + "riscv-macros", + "riscv-pac", +] + +[[package]] +name = "riscv-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "riscv-pac" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436" + +[[package]] +name = "rlsf" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1646a59a9734b8b7a0ac51689388a60fe1625d4b956348e9de07591a1478457a" +dependencies = [ + "cfg-if", + "const-default", + "libc", + "rustversion", + "svgbobdoc", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "svgbobdoc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50" +dependencies = [ + "base64", + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-width", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] diff --git a/crypto/ethrex-crypto/Cargo.toml b/crypto/ethrex-crypto/Cargo.toml new file mode 100644 index 000000000..d62dc8491 --- /dev/null +++ b/crypto/ethrex-crypto/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "lambda-vm-ethrex-crypto" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" + +# Detached workspace (like the guest): consumed by the ethrex guest as a path +# dep, and not a member of the main lambda_vm workspace (it git-deps ethrex-* +# and is riscv-oriented). +[workspace] + +# LambdaVM-side crypto accelerators for ethrex's EVM, injected into the guest as +# a `Crypto` impl. Keeping the logic here (not in the ethrex repo) means crypto +# changes don't require an ethrex PR — the guest just constructs and injects +# `LambdaVmEcsmCrypto`. See `crypto/ethrex-crypto` in the plan. + +[dependencies] +# Defines the `Crypto` trait, `CryptoError`, and `keccak::keccak_hash`. Same rev +# + `default-features = false` as the guest's ethrex-crypto, so feature +# unification adds nothing to the guest build (no C secp256k1 / malachite / kzg). +ethrex-crypto = { git = "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/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-crypto", default-features = false } +# Pinned to the exact 0.13.4 ethrex uses so the guest resolves a single k256 +# (a version split would make `FieldElement`/`Scalar` incompatible types). +# `expose-field` is required by the x-only reconstruction (Phase 1). +k256 = { version = "=0.13.4", default-features = false, features = ["arithmetic", "expose-field"] } + +# The ECSM / keccak ecalls only exist on the riscv64 guest target; on host the +# crypto methods fall back to pure-Rust k256 / software keccak, so this dep +# (which pulls riscv-only allocator crates that don't link on host) is gated out. +[target.'cfg(target_arch = "riscv64")'.dependencies] +lambda-vm-syscalls = { path = "../../syscalls" } diff --git a/crypto/ethrex-crypto/src/lib.rs b/crypto/ethrex-crypto/src/lib.rs new file mode 100644 index 000000000..be34e91b9 --- /dev/null +++ b/crypto/ethrex-crypto/src/lib.rs @@ -0,0 +1,434 @@ +//! LambdaVM crypto provider for ethrex's EVM. +//! +//! Implements ethrex's `Crypto` trait with LambdaVM precompile acceleration and +//! is injected into the ethrex guest (`Arc::new(LambdaVmEcsmCrypto)` → +//! `execution_program`). Living in the lambda_vm repo (not in ethrex) means +//! accelerator changes don't require an ethrex PR. +//! +//! Accelerated today: +//! - `keccak256`: a sponge over the `keccak_permute` precompile (riscv64; on +//! host it falls back to software keccak for tests). +//! - `secp256k1_ecrecover`: the ECDSA recovery's 2-term linear combination is +//! evaluated through the ECSM `ecsm_mul` precompile (riscv64), reconstructing +//! the full point from x-only queries; on host / degenerate inputs it falls +//! back to the pure-Rust `ProjectivePoint::lincomb`. +//! +//! Every other `Crypto` method inherits the trait default (vetted pure-Rust +//! crates: `ark-bn254`, `bls12_381`, `p256`, `sha2`, `ripemd`, …). + +use ethrex_crypto::keccak::keccak_hash; +use ethrex_crypto::{Crypto, CryptoError}; +use k256::elliptic_curve::group::prime::PrimeCurveAffine; +use k256::elliptic_curve::ops::{Invert, LinearCombination, Reduce}; +use k256::elliptic_curve::point::DecompressPoint; +use k256::elliptic_curve::sec1::ToEncodedPoint; +use k256::elliptic_curve::PrimeField; +use k256::{AffinePoint, FieldBytes, ProjectivePoint, Scalar, U256}; + +// Used only by the x-only point reconstruction (riscv accelerated path + the +// host unit tests); unused on a non-test host build. +#[cfg(any(target_arch = "riscv64", test))] +use k256::elliptic_curve::sec1::FromEncodedPoint; +#[cfg(any(target_arch = "riscv64", test))] +use k256::{EncodedPoint, FieldElement}; + +/// LambdaVM crypto provider — inject via `Arc::new(LambdaVmEcsmCrypto)`. +#[derive(Debug)] +pub struct LambdaVmEcsmCrypto; + +impl Crypto for LambdaVmEcsmCrypto { + fn secp256k1_ecrecover( + &self, + sig: &[u8; 64], + recid: u8, + msg: &[u8; 32], + ) -> Result<[u8; 32], CryptoError> { + ecsm_ecrecover(sig, recid, msg) + } + + fn keccak256(&self, input: &[u8]) -> [u8; 32] { + // riscv64 guest: sponge over the keccak_permute precompile. + #[cfg(target_arch = "riscv64")] + return keccak256_via_lambdavm(input); + // host (tests / non-guest): software keccak — the precompile syscall + // isn't available off-target. + #[cfg(not(target_arch = "riscv64"))] + return keccak_hash(input); + } +} + +// ── ECDSA secp256k1 recovery via the ECSM precompile ──────────────────────── + +/// Recover the keccak hash of the uncompressed public key from a 64-byte +/// signature, recovery id, and 32-byte message hash. Used by the ECRECOVER +/// precompile (0x01). +/// +/// Mirrors the pure-Rust recovery in the `Crypto` trait default +/// (`pk = r⁻¹·(s·R − z·G)`), but evaluates the 2-term linear combination +/// `lincomb(G, u1, R, u2)` through the ECSM accelerator via [`ecsm_lincomb2`], +/// falling back to the software `ProjectivePoint::lincomb` whenever the +/// accelerated path declines (degenerate scalars/points, or non-riscv builds). +/// We compute the recovery directly rather than calling k256's +/// `recover_from_prehash`, which internally runs a *second* lincomb to +/// re-verify the key — doubling the ECSM ecalls for no gain here. +fn ecsm_ecrecover(sig: &[u8; 64], recid: u8, msg: &[u8; 32]) -> Result<[u8; 32], CryptoError> { + let r_bytes = FieldBytes::from_slice(&sig[..32]); + let s_bytes = FieldBytes::from_slice(&sig[32..]); + + // Parse r and s as scalars, rejecting values >= the curve order. + let r: Option = Scalar::from_repr(*r_bytes).into(); + let s: Option = Scalar::from_repr(*s_bytes).into(); + let (Some(r), Some(s)) = (r, s) else { + return Err(CryptoError::InvalidSignature); + }; + if r.is_zero().into() || s.is_zero().into() { + return Err(CryptoError::InvalidSignature); + } + + // Decompress R from r and the recovery-id parity bit. + // recid >= 2 (R.x = r + n) has ~2^-128 probability and never occurs for the + // precompile; we don't handle it (decompression simply fails), matching the + // trait default. + let y_is_odd = (recid & 1) != 0; + let r_point: Option = + AffinePoint::decompress(r_bytes, u8::from(y_is_odd).into()).into(); + let Some(r_point) = r_point else { + return Err(CryptoError::RecoveryFailed); + }; + let r_proj = ProjectivePoint::from(r_point); + + let z = >::reduce_bytes(FieldBytes::from_slice(msg)); + let r_inv: Option = r.invert_vartime().into(); + let Some(r_inv) = r_inv else { + return Err(CryptoError::RecoveryFailed); + }; + let u1 = -(r_inv * z); + let u2 = r_inv * s; + + // pk = u1·G + u2·R, accelerated via ECSM with a software fallback. + let g = ProjectivePoint::GENERATOR; + let pk = ecsm_lincomb2(&g, &u1, &r_proj, &u2) + .unwrap_or_else(|| ProjectivePoint::lincomb(&g, &u1, &r_proj, &u2)); + + let pk_affine = pk.to_affine(); + if bool::from(pk_affine.is_identity()) { + return Err(CryptoError::RecoveryFailed); + } + + // SEC1 uncompressed: 0x04 || X(32) || Y(32). The address is keccak(X || Y). + let uncompressed = pk_affine.to_encoded_point(false); + Ok(keccak_hash(&uncompressed.as_bytes()[1..65])) +} + +/// ECSM-accelerated 2-term linear combination `k1·P1 + k2·P2`. +/// +/// On riscv64 this reconstructs the full affine result from four x-only ECSM +/// queries (see [`lincomb2_with_oracle`]); on other targets, and whenever a +/// degenerate-configuration guard trips, it returns `None` so the caller uses +/// the pure-Rust `ProjectivePoint::lincomb`. +#[cfg(target_arch = "riscv64")] +fn ecsm_lincomb2( + p1: &ProjectivePoint, + k1: &Scalar, + p2: &ProjectivePoint, + k2: &Scalar, +) -> Option { + lincomb2_with_oracle(p1, k1, p2, k2, ecsm_oracle) +} + +#[cfg(not(target_arch = "riscv64"))] +fn ecsm_lincomb2( + _p1: &ProjectivePoint, + _k1: &Scalar, + _p2: &ProjectivePoint, + _k2: &Scalar, +) -> Option { + None +} + +/// x-only scalar-mul oracle backed by the ECSM precompile. `x` must be the +/// x-coordinate of a curve point and `k` in `(0, n)` — guaranteed by the guards +/// in [`lincomb2_with_oracle`]. Values cross the ABI as 32-byte little-endian; +/// `xg` and `k` are distinct stack arrays so the executor's +/// `|addr_xG − addr_k| ≥ 32` assumption holds by construction. +#[cfg(target_arch = "riscv64")] +fn ecsm_oracle(x: &FieldElement, k: &Scalar) -> Option { + let x_be = x.to_bytes(); + let k_be = k.to_bytes(); + let mut x_le = [0u8; 32]; + let mut k_le = [0u8; 32]; + for i in 0..32 { + x_le[i] = x_be[31 - i]; + k_le[i] = k_be[31 - i]; + } + let mut xr_le = [0u8; 32]; + lambda_vm_syscalls::syscalls::ecsm_mul(&mut xr_le, &x_le, &k_le); + xr_le.reverse(); + Option::from(FieldElement::from_bytes(&xr_le.into())) +} + +/// Computes `k1·P1 + k2·P2` from four x-only oracle queries, or `None` if any +/// degenerate-configuration guard trips. +/// +/// The lambda-vm ECSM precompile returns only `x(k·P)`. For `A = k1·P1` with +/// `P1 = (xp, yp)` fully known, query `xa = x(k1·P1)` and `xc = x((k1+1)·P1)`. +/// The chord-addition law gives `λ² = xc + xa + xp =: t` and `ya = yp + λ·dx` +/// with `dx = xa − xp`; substituting into `ya² = xa³ + b` makes λ *linear*: +/// `λ = (xa³ − xp³ − t·dx²) / (2·yp·dx)`. The wrong sign `−ya` would force +/// `x((k1−1)·P1) = xc`, i.e. `k1 ≡ 0` or `2·k1 ≡ 0 (mod n)`, excluded by the +/// scalar guards. x-only queries are parity-invariant (`x(k·P) = x(k·(−P))`), +/// so the precompile's canonical-y lift never matters. Same for `B = k2·P2`, +/// then `Q = A + B` is one affine addition. All three inversions are batched. +/// +/// Generic over the oracle so unit tests can substitute a software stand-in. +#[cfg(any(target_arch = "riscv64", test))] +fn lincomb2_with_oracle( + p1: &ProjectivePoint, + k1: &Scalar, + p2: &ProjectivePoint, + k2: &Scalar, + oracle: O, +) -> Option +where + O: Fn(&FieldElement, &Scalar) -> Option, +{ + let a1 = p1.to_affine(); + let a2 = p2.to_affine(); + if bool::from(a1.is_identity()) || bool::from(a2.is_identity()) { + return None; + } + if scalar_near_edge(k1) || scalar_near_edge(k2) { + return None; + } + + let (x1, y1) = affine_xy(&a1)?; + let (x2, y2) = affine_xy(&a2)?; + + let xa = oracle(&x1, k1)?; + let xc1 = oracle(&x1, &(*k1 + Scalar::ONE))?; + let xb = oracle(&x2, k2)?; + let xc2 = oracle(&x2, &(*k2 + Scalar::ONE))?; + + let dx1 = (xa - x1).normalize(); + let dx2 = (xb - x2).normalize(); + let dxq = (xb - xa).normalize(); + if bool::from(dx1.is_zero()) || bool::from(dx2.is_zero()) || bool::from(dxq.is_zero()) { + return None; + } + + // One shared inversion for the two λ denominators and the final chord. + let den1 = y1.double() * dx1; + let den2 = y2.double() * dx2; + let inv = Option::::from((den1 * den2 * dxq).invert())?; + let inv_den1 = inv * den2 * dxq; + let inv_den2 = inv * den1 * dxq; + let inv_dxq = inv * den1 * den2; + + let ya = solve_y(&x1, &y1, &xa, &xc1, &dx1, &inv_den1)?; + let yb = solve_y(&x2, &y2, &xb, &xc2, &dx2, &inv_den2)?; + + // Q = A + B, with A ≠ ±B ensured by dxq ≠ 0. + let lq = (yb - ya) * inv_dxq; + let xq = (lq.square() - xa - xb).normalize(); + let yq = (lq * (xa - xq) - ya).normalize(); + + // `point_from_xy` re-validates the result is on the curve (rejecting → + // software fallback), so no separate curve-equation assertion is needed. + point_from_xy(&xq, &yq) +} + +/// Recovers `y(k·P)` for `P = (xp, yp)` from `xa = x(k·P)` and +/// `xc = x((k+1)·P)`, given `dx = xa − xp` and `inv_den = (2·yp·dx)⁻¹`. +/// `None` if the λ² consistency check fails (degenerate configuration). +#[cfg(any(target_arch = "riscv64", test))] +fn solve_y( + xp: &FieldElement, + yp: &FieldElement, + xa: &FieldElement, + xc: &FieldElement, + dx: &FieldElement, + inv_den: &FieldElement, +) -> Option { + let t = *xc + xa + xp; + let xa3 = xa.square() * xa; + let xp3 = xp.square() * xp; + let lambda = (xa3 - xp3 - t * dx.square()) * inv_den; + if lambda.square().normalize() != t.normalize() { + return None; + } + Some((*yp + lambda * dx).normalize()) +} + +/// `k ∈ {0, 1, n−1}`: cases where `k` or `k+1` is an invalid ecall scalar or +/// the chord algebra degenerates (`A = ±P`). +#[cfg(any(target_arch = "riscv64", test))] +fn scalar_near_edge(k: &Scalar) -> bool { + use k256::elliptic_curve::subtle::ConstantTimeEq; + bool::from(k.is_zero()) + || bool::from(k.ct_eq(&Scalar::ONE)) + || bool::from(k.ct_eq(&(-Scalar::ONE))) +} + +/// Affine `(x, y)` of a non-identity point as field elements, via its SEC1 +/// uncompressed encoding (k256 keeps `AffinePoint`'s coordinate fields private). +#[cfg(any(target_arch = "riscv64", test))] +fn affine_xy(p: &AffinePoint) -> Option<(FieldElement, FieldElement)> { + let ep = p.to_encoded_point(false); + let x = Option::::from(FieldElement::from_bytes(ep.x()?))?; + let y = Option::::from(FieldElement::from_bytes(ep.y()?))?; + Some((x, y)) +} + +/// Builds a curve point from affine coordinates, returning `None` if the point +/// is not on the curve (`AffinePoint::from_encoded_point` validates this). +#[cfg(any(target_arch = "riscv64", test))] +fn point_from_xy(x: &FieldElement, y: &FieldElement) -> Option { + let ep = EncodedPoint::from_affine_coordinates(&x.to_bytes(), &y.to_bytes(), false); + let affine = Option::::from(AffinePoint::from_encoded_point(&ep))?; + Some(ProjectivePoint::from(affine)) +} + +// ── Keccak-256 over the keccak_permute precompile (riscv64 guest) ─────────── + +/// Keccak-256 as a sponge over LambdaVM's `keccak_permute` syscall. +/// +/// Keccak-f[1600], rate 1088 bits (136 bytes), capacity 512 bits. +/// Padding: `0x01 ... 0x80` (multi-rate, last bit set). The state is a +/// 25-element u64 array; bytes are absorbed into the state via little-endian +/// XOR (matching the standard Keccak byte-to-lane mapping). +#[cfg(target_arch = "riscv64")] +fn keccak256_via_lambdavm(input: &[u8]) -> [u8; 32] { + const RATE: usize = 136; + + let mut state = [0u64; 25]; + let mut offset = 0; + + while input.len().saturating_sub(offset) >= RATE { + absorb_block(&mut state, &input[offset..offset + RATE]); + lambda_vm_syscalls::syscalls::keccak_permute(&mut state); + offset = offset.saturating_add(RATE); + } + + // Final block with multi-rate padding. + let mut last = [0u8; RATE]; + let remaining = input.len().saturating_sub(offset); + if let Some(tail) = last.get_mut(..remaining) { + if let Some(src) = input.get(offset..) { + tail.copy_from_slice(src); + } + } + if let Some(b) = last.get_mut(remaining) { + *b ^= 0x01; + } + if let Some(b) = last.get_mut(RATE - 1) { + *b ^= 0x80; + } + absorb_block(&mut state, &last); + lambda_vm_syscalls::syscalls::keccak_permute(&mut state); + + // Squeeze the first 32 bytes (four lanes) as little-endian. + let mut output = [0u8; 32]; + for (i, lane) in state.iter().take(4).enumerate() { + let bytes = lane.to_le_bytes(); + let start = i.saturating_mul(8); + if let Some(dst) = output.get_mut(start..start.saturating_add(8)) { + dst.copy_from_slice(&bytes); + } + } + output +} + +/// XOR one rate-sized block of bytes into the state lanes (little-endian). +#[cfg(target_arch = "riscv64")] +fn absorb_block(state: &mut [u64; 25], block: &[u8]) { + for (lane, chunk) in state.iter_mut().zip(block.chunks_exact(8)) { + let mut buf = [0u8; 8]; + buf.copy_from_slice(chunk); + *lane ^= u64::from_le_bytes(buf); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// secp256k1 curve constant `b = 7`. + fn curve_b() -> FieldElement { + let mut bytes = [0u8; 32]; + bytes[31] = 7; + FieldElement::from_bytes(&bytes.into()).unwrap() + } + + /// Software stand-in for the ECSM precompile: lift `x` to a curve point and + /// return `x(k·P)` (parity-invariant, like the real ecall). + fn soft_oracle(x: &FieldElement, k: &Scalar) -> Option { + let xn = x.normalize(); + let y2 = (xn.square() * xn + curve_b()).normalize(); + let y = Option::::from(y2.sqrt())?; + let p = point_from_xy(&xn, &y.normalize())?; + let prod = (p * k).to_affine(); + Some(affine_xy(&prod)?.0) + } + + fn g_times(n: u64) -> ProjectivePoint { + ProjectivePoint::GENERATOR * Scalar::from(n) + } + + #[test] + fn matches_software_lincomb_on_fixed_inputs() { + let cases = [ + (g_times(3), 123_456_789u64, g_times(7), 987_654_321u64), + (g_times(11), 2u64.pow(20) + 5, g_times(2), 42u64), + (ProjectivePoint::GENERATOR, 7u64, g_times(5), 9u64), + ]; + for (p1, k1, p2, k2) in cases { + let (k1, k2) = (Scalar::from(k1), Scalar::from(k2)); + let expected = ProjectivePoint::lincomb(&p1, &k1, &p2, &k2); + let got = lincomb2_with_oracle(&p1, &k1, &p2, &k2, soft_oracle) + .expect("non-degenerate inputs must reconstruct"); + assert_eq!(got.to_affine(), expected.to_affine()); + } + } + + #[test] + fn matches_software_lincomb_on_recovery_shape() { + // u1·G + u2·R, generator first, like ECDSA recovery. + let g = ProjectivePoint::GENERATOR; + let r = g_times(0x1234); + let u1 = Scalar::from(0xdead_beefu64); + let u2 = Scalar::from(0x0bad_f00du64); + let expected = ProjectivePoint::lincomb(&g, &u1, &r, &u2); + let got = lincomb2_with_oracle(&g, &u1, &r, &u2, soft_oracle) + .expect("non-degenerate inputs must reconstruct"); + assert_eq!(got.to_affine(), expected.to_affine()); + } + + #[test] + fn edge_scalars_fall_back() { + let p1 = g_times(3); + let p2 = g_times(5); + let ok = Scalar::from(12345u64); + for bad in [Scalar::ZERO, Scalar::ONE, -Scalar::ONE] { + assert!(lincomb2_with_oracle(&p1, &bad, &p2, &ok, soft_oracle).is_none()); + assert!(lincomb2_with_oracle(&p1, &ok, &p2, &bad, soft_oracle).is_none()); + } + } + + #[test] + fn identity_points_fall_back() { + let p = g_times(3); + let k = Scalar::from(7u64); + let id = ProjectivePoint::IDENTITY; + assert!(lincomb2_with_oracle(&id, &k, &p, &k, soft_oracle).is_none()); + assert!(lincomb2_with_oracle(&p, &k, &id, &k, soft_oracle).is_none()); + } + + #[test] + fn cancelling_and_doubling_terms_fall_back() { + let p = g_times(3); + let k = Scalar::from(7u64); + // A = B (doubling chord) and A = −B (Q = O): both share x(A) = x(B). + assert!(lincomb2_with_oracle(&p, &k, &p, &k, soft_oracle).is_none()); + assert!(lincomb2_with_oracle(&p, &k, &(-p), &k, soft_oracle).is_none()); + } +} diff --git a/executor/programs/rust/ethrex/Cargo.lock b/executor/programs/rust/ethrex/Cargo.lock index 58fbf4c2e..e1674f74f 100644 --- a/executor/programs/rust/ethrex/Cargo.lock +++ b/executor/programs/rust/ethrex/Cargo.lock @@ -714,6 +714,7 @@ name = "ethrex" version = "0.1.0" dependencies = [ "ethrex-guest-program", + "lambda-vm-ethrex-crypto", "lambda-vm-syscalls", "rkyv", ] @@ -1238,6 +1239,15 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" +[[package]] +name = "lambda-vm-ethrex-crypto" +version = "0.1.0" +dependencies = [ + "ethrex-crypto", + "k256", + "lambda-vm-syscalls", +] + [[package]] name = "lambda-vm-syscalls" version = "0.1.0" diff --git a/executor/programs/rust/ethrex/Cargo.toml b/executor/programs/rust/ethrex/Cargo.toml index 2cbe214b3..7d3ed7114 100644 --- a/executor/programs/rust/ethrex/Cargo.toml +++ b/executor/programs/rust/ethrex/Cargo.toml @@ -7,12 +7,16 @@ edition = "2024" [dependencies] lambda-vm-syscalls = { path = "../../../../syscalls" } +# LambdaVM crypto provider (keccak + ECSM-accelerated ecrecover), defined in the +# lambda_vm repo and injected in src/main.rs — so crypto changes stay in our repo +# and don't require an ethrex PR. +lambda-vm-ethrex-crypto = { path = "../../../../crypto/ethrex-crypto" } # Pinned by immutable `rev` to a commit on the open LambdaVM-backend PR branch # (feat/lambdavm-prover-backend) of ethrex; re-pin to the merge commit once it -# lands on ethrex `main`. The `lambdavm` feature provides -# `crypto::lambdavm::LambdaVmCrypto` (keccak via our precompile syscall; ECDSA -# and BN254 via pure-Rust crates). KZG is NOT linked under this feature (no -# kzg-rs/c-kzg in the guest Cargo.lock), so the point-evaluation precompile +# lands on ethrex `main`. The `lambdavm` feature is kept only for its dependency +# wiring (`ProgramInput`/`execution_program`/`ProgramOutput::encode` + pure-Rust +# crypto defaults); ethrex's own `LambdaVmCrypto` compiles unused — we inject our +# own. KZG is NOT linked under this feature, so the point-evaluation precompile # (0x0a) is unsupported — see src/main.rs. ethrex-guest-program = { git = "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/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-guest-program", default-features = false, features = ["lambdavm"] } # Exact pin: must match the fixture writer (tooling/ethrex-fixtures) and the diff --git a/executor/programs/rust/ethrex/src/main.rs b/executor/programs/rust/ethrex/src/main.rs index a72119416..30a39f4b5 100644 --- a/executor/programs/rust/ethrex/src/main.rs +++ b/executor/programs/rust/ethrex/src/main.rs @@ -1,22 +1,21 @@ use std::sync::Arc; -use ethrex_guest_program::crypto::lambdavm::LambdaVmCrypto; use ethrex_guest_program::l1::{ProgramInput, execution_program}; +use lambda_vm_ethrex_crypto::LambdaVmEcsmCrypto; use rkyv::rancor::Error; pub fn main() { let input = lambda_vm_syscalls::syscalls::get_private_input(); let input = rkyv::from_bytes::(&input).unwrap(); - // LambdaVM crypto provider. Only `Crypto::keccak256` routes to our - // keccak_permute precompile — ethrex's trie/RLP keccak goes through the free - // `ethrex_crypto::keccak::keccak_hash` fn, which still runs software keccak on - // riscv64, so the precompile only covers trait-routed keccak today. ECDSA and - // BN254 use pure-Rust crates; KZG is unimplemented under the `lambdavm` - // feature: blob (EIP-4844) transactions still execute (stateless block - // execution does not verify blob proofs), but a contract call to the - // point-evaluation precompile (0x0a) fails closed (reverts) instead of - // returning a result. - let crypto = Arc::new(LambdaVmCrypto); + // LambdaVM crypto provider, defined in the lambda_vm repo and injected here + // (so crypto changes don't require an ethrex PR — see `crypto/ethrex-crypto`). + // It accelerates trait-routed `keccak256` (via the keccak_permute precompile) + // and `secp256k1_ecrecover` (via the ECSM precompile); everything else uses + // ethrex's pure-Rust trait defaults. ethrex's trie/RLP keccak that goes + // through the free `keccak_hash` fn is still software, and KZG (0x0a) is + // unsupported under the `lambdavm` feature (blob txs execute; a point-eval + // precompile call reverts). + let crypto = Arc::new(LambdaVmEcsmCrypto); let output = execution_program(input, crypto).unwrap(); lambda_vm_syscalls::syscalls::commit(&output.encode()); } From 1e9f8e6ec1a9822bc5c334997598fdca0a29e33f Mon Sep 17 00:00:00 2001 From: MauroFab Date: Tue, 23 Jun 2026 18:50:50 -0300 Subject: [PATCH 2/5] Address review findings on ethrex-crypto provider - Add host-runnable end-to-end secp256k1_ecrecover known-answer tests (valid constructed ECDSA signatures + zero-r/zero-s negatives), covering the recovery wiring through the software fallback path. - Factor the keccak sponge behind an injected permutation (keccak256_with_permute) so it is host-testable against ethrex's keccak_hash across the rate/padding edge sizes. - Replace deprecated FieldBytes::from_slice with non-deprecated conversions (removes 3 build warnings). - Drop dangling "the plan" / "(Phase 1)" comment references in Cargo.toml. - Soften the point_from_xy comment: the on-curve check is a backstop, not a correctness guarantee. - Fix ecsm_oracle doc: arbitrary base point (not generator), real local names, uppercase N for the curve order. --- crypto/ethrex-crypto/Cargo.lock | 10 ++ crypto/ethrex-crypto/Cargo.toml | 8 +- crypto/ethrex-crypto/src/lib.rs | 197 +++++++++++++++++++++++++++++--- 3 files changed, 197 insertions(+), 18 deletions(-) diff --git a/crypto/ethrex-crypto/Cargo.lock b/crypto/ethrex-crypto/Cargo.lock index be8a46c1e..ec809fff9 100644 --- a/crypto/ethrex-crypto/Cargo.lock +++ b/crypto/ethrex-crypto/Cargo.lock @@ -540,12 +540,22 @@ dependencies = [ "sha2", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + [[package]] name = "lambda-vm-ethrex-crypto" version = "0.1.0" dependencies = [ "ethrex-crypto", "k256", + "keccak", "lambda-vm-syscalls", ] diff --git a/crypto/ethrex-crypto/Cargo.toml b/crypto/ethrex-crypto/Cargo.toml index d62dc8491..ea6c91074 100644 --- a/crypto/ethrex-crypto/Cargo.toml +++ b/crypto/ethrex-crypto/Cargo.toml @@ -12,7 +12,7 @@ license = "MIT OR Apache-2.0" # LambdaVM-side crypto accelerators for ethrex's EVM, injected into the guest as # a `Crypto` impl. Keeping the logic here (not in the ethrex repo) means crypto # changes don't require an ethrex PR — the guest just constructs and injects -# `LambdaVmEcsmCrypto`. See `crypto/ethrex-crypto` in the plan. +# `LambdaVmEcsmCrypto`. [dependencies] # Defines the `Crypto` trait, `CryptoError`, and `keccak::keccak_hash`. Same rev @@ -21,7 +21,7 @@ license = "MIT OR Apache-2.0" ethrex-crypto = { git = "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/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-crypto", default-features = false } # Pinned to the exact 0.13.4 ethrex uses so the guest resolves a single k256 # (a version split would make `FieldElement`/`Scalar` incompatible types). -# `expose-field` is required by the x-only reconstruction (Phase 1). +# `expose-field` is required by the x-only reconstruction. k256 = { version = "=0.13.4", default-features = false, features = ["arithmetic", "expose-field"] } # The ECSM / keccak ecalls only exist on the riscv64 guest target; on host the @@ -29,3 +29,7 @@ k256 = { version = "=0.13.4", default-features = false, features = ["arithmetic" # (which pulls riscv-only allocator crates that don't link on host) is gated out. [target.'cfg(target_arch = "riscv64")'.dependencies] lambda-vm-syscalls = { path = "../../syscalls" } + +[dev-dependencies] +# Trusted software Keccak-f[1600] used to cross-check keccak256_with_permute in tests. +keccak = "0.1" diff --git a/crypto/ethrex-crypto/src/lib.rs b/crypto/ethrex-crypto/src/lib.rs index be34e91b9..b2a539b9f 100644 --- a/crypto/ethrex-crypto/src/lib.rs +++ b/crypto/ethrex-crypto/src/lib.rs @@ -72,8 +72,8 @@ impl Crypto for LambdaVmEcsmCrypto { /// `recover_from_prehash`, which internally runs a *second* lincomb to /// re-verify the key — doubling the ECSM ecalls for no gain here. fn ecsm_ecrecover(sig: &[u8; 64], recid: u8, msg: &[u8; 32]) -> Result<[u8; 32], CryptoError> { - let r_bytes = FieldBytes::from_slice(&sig[..32]); - let s_bytes = FieldBytes::from_slice(&sig[32..]); + let r_bytes = <&FieldBytes>::from(&sig[..32]); + let s_bytes = <&FieldBytes>::from(&sig[32..]); // Parse r and s as scalars, rejecting values >= the curve order. let r: Option = Scalar::from_repr(*r_bytes).into(); @@ -97,7 +97,7 @@ fn ecsm_ecrecover(sig: &[u8; 64], recid: u8, msg: &[u8; 32]) -> Result<[u8; 32], }; let r_proj = ProjectivePoint::from(r_point); - let z = >::reduce_bytes(FieldBytes::from_slice(msg)); + let z = >::reduce_bytes(&FieldBytes::from(*msg)); let r_inv: Option = r.invert_vartime().into(); let Some(r_inv) = r_inv else { return Err(CryptoError::RecoveryFailed); @@ -146,11 +146,13 @@ fn ecsm_lincomb2( None } -/// x-only scalar-mul oracle backed by the ECSM precompile. `x` must be the -/// x-coordinate of a curve point and `k` in `(0, n)` — guaranteed by the guards -/// in [`lincomb2_with_oracle`]. Values cross the ABI as 32-byte little-endian; -/// `xg` and `k` are distinct stack arrays so the executor's -/// `|addr_xG − addr_k| ≥ 32` assumption holds by construction. +/// x-only scalar-mul oracle backed by the ECSM precompile: computes `x(k·P)` +/// for the curve point P whose x-coordinate is passed in. `x` must be the +/// x-coordinate of a curve point and `k` in `(0, N)` (N = curve order) — +/// guaranteed by the guards in [`lincomb2_with_oracle`]. Values cross the ABI +/// as 32-byte little-endian; `x_le` and `k_le` are distinct stack arrays so +/// the executor's `|addr_x_le − addr_k_le| ≥ 32` assumption holds by +/// construction. #[cfg(target_arch = "riscv64")] fn ecsm_oracle(x: &FieldElement, k: &Scalar) -> Option { let x_be = x.to_bytes(); @@ -232,8 +234,10 @@ where let xq = (lq.square() - xa - xb).normalize(); let yq = (lq * (xa - xq) - ya).normalize(); - // `point_from_xy` re-validates the result is on the curve (rejecting → - // software fallback), so no separate curve-equation assertion is needed. + // `point_from_xy` checks the result is on the curve as a cheap backstop: + // it rejects gross off-curve garbage and falls back to software, but + // correctness rests on the algebra above — an on-curve-but-wrong point + // would still pass this check. point_from_xy(&xq, &yq) } @@ -290,14 +294,17 @@ fn point_from_xy(x: &FieldElement, y: &FieldElement) -> Option // ── Keccak-256 over the keccak_permute precompile (riscv64 guest) ─────────── -/// Keccak-256 as a sponge over LambdaVM's `keccak_permute` syscall. +/// Keccak-256 sponge with an injected permutation function. /// /// Keccak-f[1600], rate 1088 bits (136 bytes), capacity 512 bits. /// Padding: `0x01 ... 0x80` (multi-rate, last bit set). The state is a /// 25-element u64 array; bytes are absorbed into the state via little-endian /// XOR (matching the standard Keccak byte-to-lane mapping). -#[cfg(target_arch = "riscv64")] -fn keccak256_via_lambdavm(input: &[u8]) -> [u8; 32] { +/// +/// Gated to `riscv64 | test` so the generic function is available to the host +/// unit tests without being dead code in the non-test host build. +#[cfg(any(target_arch = "riscv64", test))] +fn keccak256_with_permute(input: &[u8], mut permute: F) -> [u8; 32] { const RATE: usize = 136; let mut state = [0u64; 25]; @@ -305,7 +312,7 @@ fn keccak256_via_lambdavm(input: &[u8]) -> [u8; 32] { while input.len().saturating_sub(offset) >= RATE { absorb_block(&mut state, &input[offset..offset + RATE]); - lambda_vm_syscalls::syscalls::keccak_permute(&mut state); + permute(&mut state); offset = offset.saturating_add(RATE); } @@ -324,7 +331,7 @@ fn keccak256_via_lambdavm(input: &[u8]) -> [u8; 32] { *b ^= 0x80; } absorb_block(&mut state, &last); - lambda_vm_syscalls::syscalls::keccak_permute(&mut state); + permute(&mut state); // Squeeze the first 32 bytes (four lanes) as little-endian. let mut output = [0u8; 32]; @@ -338,8 +345,14 @@ fn keccak256_via_lambdavm(input: &[u8]) -> [u8; 32] { output } -/// XOR one rate-sized block of bytes into the state lanes (little-endian). +/// Keccak-256 via LambdaVM's `keccak_permute` syscall (riscv64 guest only). #[cfg(target_arch = "riscv64")] +fn keccak256_via_lambdavm(input: &[u8]) -> [u8; 32] { + keccak256_with_permute(input, |s| lambda_vm_syscalls::syscalls::keccak_permute(s)) +} + +/// XOR one rate-sized block of bytes into the state lanes (little-endian). +#[cfg(any(target_arch = "riscv64", test))] fn absorb_block(state: &mut [u64; 25], block: &[u8]) { for (lane, chunk) in state.iter_mut().zip(block.chunks_exact(8)) { let mut buf = [0u8; 8]; @@ -431,4 +444,156 @@ mod tests { assert!(lincomb2_with_oracle(&p, &k, &p, &k, soft_oracle).is_none()); assert!(lincomb2_with_oracle(&p, &k, &(-p), &k, soft_oracle).is_none()); } + + // ── Issue 1: known-answer tests for the full ecsm_ecrecover path ───────── + + /// Build a valid ECDSA/secp256k1 signature from (d, kk, msg) using only the + /// k256 primitives already imported and return `(sig, recid, expected_addr)`. + /// + /// `expected_addr` = keccak(X‖Y) of the uncompressed public key, exactly as + /// `ecsm_ecrecover` computes it. + fn make_ecdsa_fixture( + d: Scalar, + kk: Scalar, + msg: [u8; 32], + ) -> ([u8; 64], u8, [u8; 32]) { + assert!(!bool::from(d.is_zero()), "private key must be nonzero"); + assert!(!bool::from(kk.is_zero()), "nonce must be nonzero"); + + // Public key Q = d·G. + let q = (ProjectivePoint::GENERATOR * d).to_affine(); + let q_uncompressed = q.to_encoded_point(false); + let expected = keccak_hash(&q_uncompressed.as_bytes()[1..65]); + + // R = kk·G; r = reduce(Rx); assert r ≠ 0. + let r_point = (ProjectivePoint::GENERATOR * kk).to_affine(); + let (rx, ry) = affine_xy(&r_point).expect("R is not identity"); + let r = >::reduce_bytes(&rx.to_bytes()); + assert!(!bool::from(r.is_zero()), "r must be nonzero"); + + // recid parity: low bit of Ry (big-endian, byte 31). + let recid = ry.normalize().to_bytes()[31] & 1; + + // z = reduce(msg). + let z = >::reduce_bytes(&FieldBytes::from(msg)); + + // s = kk⁻¹ · (z + r·d). + let s = kk.invert_vartime().expect("kk is nonzero") * (z + r * d); + assert!(!bool::from(s.is_zero()), "s must be nonzero"); + + // sig = r (BE, 32 bytes) ‖ s (BE, 32 bytes). + let mut sig = [0u8; 64]; + sig[..32].copy_from_slice(&r.to_bytes()); + sig[32..].copy_from_slice(&s.to_bytes()); + + (sig, recid, expected) + } + + #[test] + fn ecrecover_known_answer_three_tuples() { + // Three distinct (d, kk, msg) tuples — deterministic, no RNG. + let tuples: &[(u64, u64, [u8; 32])] = &[ + ( + 0x0000_0000_0000_0001u64, + 0x0000_0000_dead_beefu64, + { + let mut m = [0u8; 32]; + m[31] = 0x42; + m + }, + ), + ( + 0x00c0_ffee_dead_beef_u64, + 0x0123_4567_89ab_cdef_u64, + { + let mut m = [0u8; 32]; + m[0] = 0xff; + m[31] = 0x01; + m + }, + ), + ( + 0x0bad_f00d_1337_cafe, + 0xfeed_face_0000_0001, + { + let mut m = [0u8; 32]; + for (i, b) in m.iter_mut().enumerate() { + *b = i as u8; + } + m + }, + ), + ]; + + for &(d_u64, kk_u64, msg) in tuples { + let d = Scalar::from(d_u64); + let kk = Scalar::from(kk_u64); + let (sig, recid, expected) = make_ecdsa_fixture(d, kk, msg); + match ecsm_ecrecover(&sig, recid, &msg) { + Ok(got) => assert_eq!( + got, expected, + "ecrecover returned wrong address for d={d_u64:#x} kk={kk_u64:#x}" + ), + Err(e) => panic!( + "ecrecover failed for d={d_u64:#x} kk={kk_u64:#x}: {e:?}" + ), + } + } + } + + #[test] + fn ecrecover_rejects_zero_s() { + // sig = valid r ‖ 0x00..00 (s = 0) must return InvalidSignature. + let mut sig = [0u8; 64]; + // r = 1 (nonzero, but s = 0 in the second half). + sig[31] = 0x01; + let msg = [0u8; 32]; + assert!( + matches!(ecsm_ecrecover(&sig, 0, &msg), Err(CryptoError::InvalidSignature)), + "expected InvalidSignature for zero s" + ); + } + + #[test] + fn ecrecover_rejects_zero_r() { + // sig = 0x00..00 ‖ valid s must return InvalidSignature. + let mut sig = [0u8; 64]; + sig[63] = 0x01; // s = 1, r = 0 + let msg = [0u8; 32]; + assert!( + matches!(ecsm_ecrecover(&sig, 0, &msg), Err(CryptoError::InvalidSignature)), + "expected InvalidSignature for zero r" + ); + } + + // ── Issue 2: host-side Keccak sponge tests via injected permutation ─────── + + /// Cross-check our sponge body against the trusted `keccak` crate's f1600. + fn check_keccak(input: &[u8]) { + let got = keccak256_with_permute(input, keccak::f1600); + let want = keccak_hash(input); + assert_eq!( + got, want, + "keccak256 mismatch for {}-byte input", + input.len() + ); + } + + #[test] + fn keccak_sponge_matches_trusted_permutation() { + // Empty input. + check_keccak(&[]); + // One byte. + check_keccak(&[0xab]); + // 135 bytes — RATE-1: padding lands on byte 135 (0x01) and byte 135 is + // also the last byte (0x80), so both bits land on the same byte: 0x81. + check_keccak(&[0x5a; 135]); + // Exactly RATE (136): fills one full block, final block is all-padding. + check_keccak(&[0x3c; 136]); + // RATE+1: one full block + one-byte remainder. + check_keccak(&[0x7e; 137]); + // Multi-block: ~1.5 × RATE (200 bytes), deterministic pattern. + let long: Vec = (0u8..200).collect(); + check_keccak(&long); + } } From c5d10d76f92f2eda521e1ede5de4db5ddd3ab4e9 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Tue, 23 Jun 2026 18:55:26 -0300 Subject: [PATCH 3/5] Move ethrex-crypto tests into src/tests/ modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the repo test-layout convention (prover/executor src/tests/): split the inline `mod tests` in lib.rs into per-area files under src/tests/ declared from src/tests/mod.rs. - src/tests/ecsm_tests.rs — x-only lincomb2 reconstruction + fallbacks - src/tests/ecrecover_tests.rs — full ecsm_ecrecover known-answer/negatives - src/tests/keccak_tests.rs — keccak sponge vs reference keccak_hash --- crypto/ethrex-crypto/src/lib.rs | 236 +----------------- .../src/tests/ecrecover_tests.rs | 126 ++++++++++ crypto/ethrex-crypto/src/tests/ecsm_tests.rs | 85 +++++++ .../ethrex-crypto/src/tests/keccak_tests.rs | 30 +++ crypto/ethrex-crypto/src/tests/mod.rs | 6 + 5 files changed, 248 insertions(+), 235 deletions(-) create mode 100644 crypto/ethrex-crypto/src/tests/ecrecover_tests.rs create mode 100644 crypto/ethrex-crypto/src/tests/ecsm_tests.rs create mode 100644 crypto/ethrex-crypto/src/tests/keccak_tests.rs create mode 100644 crypto/ethrex-crypto/src/tests/mod.rs diff --git a/crypto/ethrex-crypto/src/lib.rs b/crypto/ethrex-crypto/src/lib.rs index b2a539b9f..1981ac747 100644 --- a/crypto/ethrex-crypto/src/lib.rs +++ b/crypto/ethrex-crypto/src/lib.rs @@ -362,238 +362,4 @@ fn absorb_block(state: &mut [u64; 25], block: &[u8]) { } #[cfg(test)] -mod tests { - use super::*; - - /// secp256k1 curve constant `b = 7`. - fn curve_b() -> FieldElement { - let mut bytes = [0u8; 32]; - bytes[31] = 7; - FieldElement::from_bytes(&bytes.into()).unwrap() - } - - /// Software stand-in for the ECSM precompile: lift `x` to a curve point and - /// return `x(k·P)` (parity-invariant, like the real ecall). - fn soft_oracle(x: &FieldElement, k: &Scalar) -> Option { - let xn = x.normalize(); - let y2 = (xn.square() * xn + curve_b()).normalize(); - let y = Option::::from(y2.sqrt())?; - let p = point_from_xy(&xn, &y.normalize())?; - let prod = (p * k).to_affine(); - Some(affine_xy(&prod)?.0) - } - - fn g_times(n: u64) -> ProjectivePoint { - ProjectivePoint::GENERATOR * Scalar::from(n) - } - - #[test] - fn matches_software_lincomb_on_fixed_inputs() { - let cases = [ - (g_times(3), 123_456_789u64, g_times(7), 987_654_321u64), - (g_times(11), 2u64.pow(20) + 5, g_times(2), 42u64), - (ProjectivePoint::GENERATOR, 7u64, g_times(5), 9u64), - ]; - for (p1, k1, p2, k2) in cases { - let (k1, k2) = (Scalar::from(k1), Scalar::from(k2)); - let expected = ProjectivePoint::lincomb(&p1, &k1, &p2, &k2); - let got = lincomb2_with_oracle(&p1, &k1, &p2, &k2, soft_oracle) - .expect("non-degenerate inputs must reconstruct"); - assert_eq!(got.to_affine(), expected.to_affine()); - } - } - - #[test] - fn matches_software_lincomb_on_recovery_shape() { - // u1·G + u2·R, generator first, like ECDSA recovery. - let g = ProjectivePoint::GENERATOR; - let r = g_times(0x1234); - let u1 = Scalar::from(0xdead_beefu64); - let u2 = Scalar::from(0x0bad_f00du64); - let expected = ProjectivePoint::lincomb(&g, &u1, &r, &u2); - let got = lincomb2_with_oracle(&g, &u1, &r, &u2, soft_oracle) - .expect("non-degenerate inputs must reconstruct"); - assert_eq!(got.to_affine(), expected.to_affine()); - } - - #[test] - fn edge_scalars_fall_back() { - let p1 = g_times(3); - let p2 = g_times(5); - let ok = Scalar::from(12345u64); - for bad in [Scalar::ZERO, Scalar::ONE, -Scalar::ONE] { - assert!(lincomb2_with_oracle(&p1, &bad, &p2, &ok, soft_oracle).is_none()); - assert!(lincomb2_with_oracle(&p1, &ok, &p2, &bad, soft_oracle).is_none()); - } - } - - #[test] - fn identity_points_fall_back() { - let p = g_times(3); - let k = Scalar::from(7u64); - let id = ProjectivePoint::IDENTITY; - assert!(lincomb2_with_oracle(&id, &k, &p, &k, soft_oracle).is_none()); - assert!(lincomb2_with_oracle(&p, &k, &id, &k, soft_oracle).is_none()); - } - - #[test] - fn cancelling_and_doubling_terms_fall_back() { - let p = g_times(3); - let k = Scalar::from(7u64); - // A = B (doubling chord) and A = −B (Q = O): both share x(A) = x(B). - assert!(lincomb2_with_oracle(&p, &k, &p, &k, soft_oracle).is_none()); - assert!(lincomb2_with_oracle(&p, &k, &(-p), &k, soft_oracle).is_none()); - } - - // ── Issue 1: known-answer tests for the full ecsm_ecrecover path ───────── - - /// Build a valid ECDSA/secp256k1 signature from (d, kk, msg) using only the - /// k256 primitives already imported and return `(sig, recid, expected_addr)`. - /// - /// `expected_addr` = keccak(X‖Y) of the uncompressed public key, exactly as - /// `ecsm_ecrecover` computes it. - fn make_ecdsa_fixture( - d: Scalar, - kk: Scalar, - msg: [u8; 32], - ) -> ([u8; 64], u8, [u8; 32]) { - assert!(!bool::from(d.is_zero()), "private key must be nonzero"); - assert!(!bool::from(kk.is_zero()), "nonce must be nonzero"); - - // Public key Q = d·G. - let q = (ProjectivePoint::GENERATOR * d).to_affine(); - let q_uncompressed = q.to_encoded_point(false); - let expected = keccak_hash(&q_uncompressed.as_bytes()[1..65]); - - // R = kk·G; r = reduce(Rx); assert r ≠ 0. - let r_point = (ProjectivePoint::GENERATOR * kk).to_affine(); - let (rx, ry) = affine_xy(&r_point).expect("R is not identity"); - let r = >::reduce_bytes(&rx.to_bytes()); - assert!(!bool::from(r.is_zero()), "r must be nonzero"); - - // recid parity: low bit of Ry (big-endian, byte 31). - let recid = ry.normalize().to_bytes()[31] & 1; - - // z = reduce(msg). - let z = >::reduce_bytes(&FieldBytes::from(msg)); - - // s = kk⁻¹ · (z + r·d). - let s = kk.invert_vartime().expect("kk is nonzero") * (z + r * d); - assert!(!bool::from(s.is_zero()), "s must be nonzero"); - - // sig = r (BE, 32 bytes) ‖ s (BE, 32 bytes). - let mut sig = [0u8; 64]; - sig[..32].copy_from_slice(&r.to_bytes()); - sig[32..].copy_from_slice(&s.to_bytes()); - - (sig, recid, expected) - } - - #[test] - fn ecrecover_known_answer_three_tuples() { - // Three distinct (d, kk, msg) tuples — deterministic, no RNG. - let tuples: &[(u64, u64, [u8; 32])] = &[ - ( - 0x0000_0000_0000_0001u64, - 0x0000_0000_dead_beefu64, - { - let mut m = [0u8; 32]; - m[31] = 0x42; - m - }, - ), - ( - 0x00c0_ffee_dead_beef_u64, - 0x0123_4567_89ab_cdef_u64, - { - let mut m = [0u8; 32]; - m[0] = 0xff; - m[31] = 0x01; - m - }, - ), - ( - 0x0bad_f00d_1337_cafe, - 0xfeed_face_0000_0001, - { - let mut m = [0u8; 32]; - for (i, b) in m.iter_mut().enumerate() { - *b = i as u8; - } - m - }, - ), - ]; - - for &(d_u64, kk_u64, msg) in tuples { - let d = Scalar::from(d_u64); - let kk = Scalar::from(kk_u64); - let (sig, recid, expected) = make_ecdsa_fixture(d, kk, msg); - match ecsm_ecrecover(&sig, recid, &msg) { - Ok(got) => assert_eq!( - got, expected, - "ecrecover returned wrong address for d={d_u64:#x} kk={kk_u64:#x}" - ), - Err(e) => panic!( - "ecrecover failed for d={d_u64:#x} kk={kk_u64:#x}: {e:?}" - ), - } - } - } - - #[test] - fn ecrecover_rejects_zero_s() { - // sig = valid r ‖ 0x00..00 (s = 0) must return InvalidSignature. - let mut sig = [0u8; 64]; - // r = 1 (nonzero, but s = 0 in the second half). - sig[31] = 0x01; - let msg = [0u8; 32]; - assert!( - matches!(ecsm_ecrecover(&sig, 0, &msg), Err(CryptoError::InvalidSignature)), - "expected InvalidSignature for zero s" - ); - } - - #[test] - fn ecrecover_rejects_zero_r() { - // sig = 0x00..00 ‖ valid s must return InvalidSignature. - let mut sig = [0u8; 64]; - sig[63] = 0x01; // s = 1, r = 0 - let msg = [0u8; 32]; - assert!( - matches!(ecsm_ecrecover(&sig, 0, &msg), Err(CryptoError::InvalidSignature)), - "expected InvalidSignature for zero r" - ); - } - - // ── Issue 2: host-side Keccak sponge tests via injected permutation ─────── - - /// Cross-check our sponge body against the trusted `keccak` crate's f1600. - fn check_keccak(input: &[u8]) { - let got = keccak256_with_permute(input, keccak::f1600); - let want = keccak_hash(input); - assert_eq!( - got, want, - "keccak256 mismatch for {}-byte input", - input.len() - ); - } - - #[test] - fn keccak_sponge_matches_trusted_permutation() { - // Empty input. - check_keccak(&[]); - // One byte. - check_keccak(&[0xab]); - // 135 bytes — RATE-1: padding lands on byte 135 (0x01) and byte 135 is - // also the last byte (0x80), so both bits land on the same byte: 0x81. - check_keccak(&[0x5a; 135]); - // Exactly RATE (136): fills one full block, final block is all-padding. - check_keccak(&[0x3c; 136]); - // RATE+1: one full block + one-byte remainder. - check_keccak(&[0x7e; 137]); - // Multi-block: ~1.5 × RATE (200 bytes), deterministic pattern. - let long: Vec = (0u8..200).collect(); - check_keccak(&long); - } -} +mod tests; diff --git a/crypto/ethrex-crypto/src/tests/ecrecover_tests.rs b/crypto/ethrex-crypto/src/tests/ecrecover_tests.rs new file mode 100644 index 000000000..ec3670d19 --- /dev/null +++ b/crypto/ethrex-crypto/src/tests/ecrecover_tests.rs @@ -0,0 +1,126 @@ +//! Known-answer tests for the full `ecsm_ecrecover` path (r/s parse, +//! decompress + parity, z-reduction, u1/u2, final keccak(X‖Y) address). +//! +//! On host, `ecsm_lincomb2` returns `None`, so these exercise the recovery +//! wiring through the pure-Rust `ProjectivePoint::lincomb` fallback. + +use crate::*; + +/// Build a valid ECDSA/secp256k1 signature from (d, kk, msg) using only the +/// k256 primitives already imported and return `(sig, recid, expected_addr)`. +/// +/// `expected_addr` = keccak(X‖Y) of the uncompressed public key, exactly as +/// `ecsm_ecrecover` computes it. +fn make_ecdsa_fixture(d: Scalar, kk: Scalar, msg: [u8; 32]) -> ([u8; 64], u8, [u8; 32]) { + assert!(!bool::from(d.is_zero()), "private key must be nonzero"); + assert!(!bool::from(kk.is_zero()), "nonce must be nonzero"); + + // Public key Q = d·G. + let q = (ProjectivePoint::GENERATOR * d).to_affine(); + let q_uncompressed = q.to_encoded_point(false); + let expected = keccak_hash(&q_uncompressed.as_bytes()[1..65]); + + // R = kk·G; r = reduce(Rx); assert r ≠ 0. + let r_point = (ProjectivePoint::GENERATOR * kk).to_affine(); + let (rx, ry) = affine_xy(&r_point).expect("R is not identity"); + let r = >::reduce_bytes(&rx.to_bytes()); + assert!(!bool::from(r.is_zero()), "r must be nonzero"); + + // recid parity: low bit of Ry (big-endian, byte 31). + let recid = ry.normalize().to_bytes()[31] & 1; + + // z = reduce(msg). + let z = >::reduce_bytes(&FieldBytes::from(msg)); + + // s = kk⁻¹ · (z + r·d). + let s = kk.invert_vartime().expect("kk is nonzero") * (z + r * d); + assert!(!bool::from(s.is_zero()), "s must be nonzero"); + + // sig = r (BE, 32 bytes) ‖ s (BE, 32 bytes). + let mut sig = [0u8; 64]; + sig[..32].copy_from_slice(&r.to_bytes()); + sig[32..].copy_from_slice(&s.to_bytes()); + + (sig, recid, expected) +} + +#[test] +fn ecrecover_known_answer_three_tuples() { + // Three distinct (d, kk, msg) tuples — deterministic, no RNG. + let tuples: &[(u64, u64, [u8; 32])] = &[ + ( + 0x0000_0000_0000_0001u64, + 0x0000_0000_dead_beefu64, + { + let mut m = [0u8; 32]; + m[31] = 0x42; + m + }, + ), + ( + 0x00c0_ffee_dead_beef_u64, + 0x0123_4567_89ab_cdef_u64, + { + let mut m = [0u8; 32]; + m[0] = 0xff; + m[31] = 0x01; + m + }, + ), + ( + 0x0bad_f00d_1337_cafe, + 0xfeed_face_0000_0001, + { + let mut m = [0u8; 32]; + for (i, b) in m.iter_mut().enumerate() { + *b = i as u8; + } + m + }, + ), + ]; + + for &(d_u64, kk_u64, msg) in tuples { + let d = Scalar::from(d_u64); + let kk = Scalar::from(kk_u64); + let (sig, recid, expected) = make_ecdsa_fixture(d, kk, msg); + match ecsm_ecrecover(&sig, recid, &msg) { + Ok(got) => assert_eq!( + got, expected, + "ecrecover returned wrong address for d={d_u64:#x} kk={kk_u64:#x}" + ), + Err(e) => panic!("ecrecover failed for d={d_u64:#x} kk={kk_u64:#x}: {e:?}"), + } + } +} + +#[test] +fn ecrecover_rejects_zero_s() { + // sig = valid r ‖ 0x00..00 (s = 0) must return InvalidSignature. + let mut sig = [0u8; 64]; + // r = 1 (nonzero, but s = 0 in the second half). + sig[31] = 0x01; + let msg = [0u8; 32]; + assert!( + matches!( + ecsm_ecrecover(&sig, 0, &msg), + Err(CryptoError::InvalidSignature) + ), + "expected InvalidSignature for zero s" + ); +} + +#[test] +fn ecrecover_rejects_zero_r() { + // sig = 0x00..00 ‖ valid s must return InvalidSignature. + let mut sig = [0u8; 64]; + sig[63] = 0x01; // s = 1, r = 0 + let msg = [0u8; 32]; + assert!( + matches!( + ecsm_ecrecover(&sig, 0, &msg), + Err(CryptoError::InvalidSignature) + ), + "expected InvalidSignature for zero r" + ); +} diff --git a/crypto/ethrex-crypto/src/tests/ecsm_tests.rs b/crypto/ethrex-crypto/src/tests/ecsm_tests.rs new file mode 100644 index 000000000..08f5a20f6 --- /dev/null +++ b/crypto/ethrex-crypto/src/tests/ecsm_tests.rs @@ -0,0 +1,85 @@ +//! Tests for the x-only ECSM linear-combination reconstruction +//! (`lincomb2_with_oracle`) against the software `ProjectivePoint::lincomb`, +//! plus the degenerate-configuration fallback guards. + +use crate::*; + +/// secp256k1 curve constant `b = 7`. +fn curve_b() -> FieldElement { + let mut bytes = [0u8; 32]; + bytes[31] = 7; + FieldElement::from_bytes(&bytes.into()).unwrap() +} + +/// Software stand-in for the ECSM precompile: lift `x` to a curve point and +/// return `x(k·P)` (parity-invariant, like the real ecall). +fn soft_oracle(x: &FieldElement, k: &Scalar) -> Option { + let xn = x.normalize(); + let y2 = (xn.square() * xn + curve_b()).normalize(); + let y = Option::::from(y2.sqrt())?; + let p = point_from_xy(&xn, &y.normalize())?; + let prod = (p * k).to_affine(); + Some(affine_xy(&prod)?.0) +} + +fn g_times(n: u64) -> ProjectivePoint { + ProjectivePoint::GENERATOR * Scalar::from(n) +} + +#[test] +fn matches_software_lincomb_on_fixed_inputs() { + let cases = [ + (g_times(3), 123_456_789u64, g_times(7), 987_654_321u64), + (g_times(11), 2u64.pow(20) + 5, g_times(2), 42u64), + (ProjectivePoint::GENERATOR, 7u64, g_times(5), 9u64), + ]; + for (p1, k1, p2, k2) in cases { + let (k1, k2) = (Scalar::from(k1), Scalar::from(k2)); + let expected = ProjectivePoint::lincomb(&p1, &k1, &p2, &k2); + let got = lincomb2_with_oracle(&p1, &k1, &p2, &k2, soft_oracle) + .expect("non-degenerate inputs must reconstruct"); + assert_eq!(got.to_affine(), expected.to_affine()); + } +} + +#[test] +fn matches_software_lincomb_on_recovery_shape() { + // u1·G + u2·R, generator first, like ECDSA recovery. + let g = ProjectivePoint::GENERATOR; + let r = g_times(0x1234); + let u1 = Scalar::from(0xdead_beefu64); + let u2 = Scalar::from(0x0bad_f00du64); + let expected = ProjectivePoint::lincomb(&g, &u1, &r, &u2); + let got = lincomb2_with_oracle(&g, &u1, &r, &u2, soft_oracle) + .expect("non-degenerate inputs must reconstruct"); + assert_eq!(got.to_affine(), expected.to_affine()); +} + +#[test] +fn edge_scalars_fall_back() { + let p1 = g_times(3); + let p2 = g_times(5); + let ok = Scalar::from(12345u64); + for bad in [Scalar::ZERO, Scalar::ONE, -Scalar::ONE] { + assert!(lincomb2_with_oracle(&p1, &bad, &p2, &ok, soft_oracle).is_none()); + assert!(lincomb2_with_oracle(&p1, &ok, &p2, &bad, soft_oracle).is_none()); + } +} + +#[test] +fn identity_points_fall_back() { + let p = g_times(3); + let k = Scalar::from(7u64); + let id = ProjectivePoint::IDENTITY; + assert!(lincomb2_with_oracle(&id, &k, &p, &k, soft_oracle).is_none()); + assert!(lincomb2_with_oracle(&p, &k, &id, &k, soft_oracle).is_none()); +} + +#[test] +fn cancelling_and_doubling_terms_fall_back() { + let p = g_times(3); + let k = Scalar::from(7u64); + // A = B (doubling chord) and A = −B (Q = O): both share x(A) = x(B). + assert!(lincomb2_with_oracle(&p, &k, &p, &k, soft_oracle).is_none()); + assert!(lincomb2_with_oracle(&p, &k, &(-p), &k, soft_oracle).is_none()); +} diff --git a/crypto/ethrex-crypto/src/tests/keccak_tests.rs b/crypto/ethrex-crypto/src/tests/keccak_tests.rs new file mode 100644 index 000000000..efa526674 --- /dev/null +++ b/crypto/ethrex-crypto/src/tests/keccak_tests.rs @@ -0,0 +1,30 @@ +//! Host-side tests for the Keccak-256 sponge (`keccak256_with_permute`), +//! driving it with the trusted `keccak` crate's f1600 permutation and +//! cross-checking against ethrex's reference `keccak_hash`. + +use crate::*; + +/// Cross-check our sponge body against the trusted `keccak` crate's f1600. +fn check_keccak(input: &[u8]) { + let got = keccak256_with_permute(input, keccak::f1600); + let want = keccak_hash(input); + assert_eq!(got, want, "keccak256 mismatch for {}-byte input", input.len()); +} + +#[test] +fn keccak_sponge_matches_trusted_permutation() { + // Empty input. + check_keccak(&[]); + // One byte. + check_keccak(&[0xab]); + // 135 bytes — RATE-1: padding lands on byte 135 (0x01) and byte 135 is + // also the last byte (0x80), so both bits land on the same byte: 0x81. + check_keccak(&[0x5a; 135]); + // Exactly RATE (136): fills one full block, final block is all-padding. + check_keccak(&[0x3c; 136]); + // RATE+1: one full block + one-byte remainder. + check_keccak(&[0x7e; 137]); + // Multi-block: ~1.5 × RATE (200 bytes), deterministic pattern. + let long: Vec = (0u8..200).collect(); + check_keccak(&long); +} diff --git a/crypto/ethrex-crypto/src/tests/mod.rs b/crypto/ethrex-crypto/src/tests/mod.rs new file mode 100644 index 000000000..f050a8e48 --- /dev/null +++ b/crypto/ethrex-crypto/src/tests/mod.rs @@ -0,0 +1,6 @@ +#[cfg(test)] +pub mod ecrecover_tests; +#[cfg(test)] +pub mod ecsm_tests; +#[cfg(test)] +pub mod keccak_tests; From fcd26810372942f2ee9a03b09e090b1f135154d8 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 24 Jun 2026 09:56:18 -0300 Subject: [PATCH 4/5] Add edge-case tests and fix misleading comments --- crypto/ethrex-crypto/src/lib.rs | 40 +++----- .../src/tests/ecrecover_tests.rs | 9 ++ crypto/ethrex-crypto/src/tests/ecsm_tests.rs | 92 +++++++++++++++++++ .../ethrex-crypto/src/tests/keccak_tests.rs | 43 +++++++++ 4 files changed, 158 insertions(+), 26 deletions(-) diff --git a/crypto/ethrex-crypto/src/lib.rs b/crypto/ethrex-crypto/src/lib.rs index 1981ac747..76d24f045 100644 --- a/crypto/ethrex-crypto/src/lib.rs +++ b/crypto/ethrex-crypto/src/lib.rs @@ -124,8 +124,8 @@ fn ecsm_ecrecover(sig: &[u8; 64], recid: u8, msg: &[u8; 32]) -> Result<[u8; 32], /// /// On riscv64 this reconstructs the full affine result from four x-only ECSM /// queries (see [`lincomb2_with_oracle`]); on other targets, and whenever a -/// degenerate-configuration guard trips, it returns `None` so the caller uses -/// the pure-Rust `ProjectivePoint::lincomb`. +/// guard trips (degenerate input or oracle inconsistency), it returns `None` +/// so the caller uses the pure-Rust `ProjectivePoint::lincomb`. #[cfg(target_arch = "riscv64")] fn ecsm_lincomb2( p1: &ProjectivePoint, @@ -241,9 +241,9 @@ where point_from_xy(&xq, &yq) } -/// Recovers `y(k·P)` for `P = (xp, yp)` from `xa = x(k·P)` and -/// `xc = x((k+1)·P)`, given `dx = xa − xp` and `inv_den = (2·yp·dx)⁻¹`. -/// `None` if the λ² consistency check fails (degenerate configuration). +/// Recovers `y(k·P)` from `xa = x(k·P)` and `xc = x((k+1)·P)`. +/// Returns `None` if `xc` is inconsistent with the computed `lambda` +/// (oracle misbehavior); degeneracy guards are in [`lincomb2_with_oracle`]. #[cfg(any(target_arch = "riscv64", test))] fn solve_y( xp: &FieldElement, @@ -263,8 +263,8 @@ fn solve_y( Some((*yp + lambda * dx).normalize()) } -/// `k ∈ {0, 1, n−1}`: cases where `k` or `k+1` is an invalid ecall scalar or -/// the chord algebra degenerates (`A = ±P`). +/// `k ∈ {0, 1, n−1}`: fast early-exit before oracle calls. +/// k=0: invalid ecall scalar. k=1: dx=0. k=n-1: k+1 wraps to 0 mod n. #[cfg(any(target_arch = "riscv64", test))] fn scalar_near_edge(k: &Scalar) -> bool { use k256::elliptic_curve::subtle::ConstantTimeEq; @@ -310,37 +310,25 @@ fn keccak256_with_permute(input: &[u8], mut permute: F let mut state = [0u64; 25]; let mut offset = 0; - while input.len().saturating_sub(offset) >= RATE { + while input.len() - offset >= RATE { absorb_block(&mut state, &input[offset..offset + RATE]); permute(&mut state); - offset = offset.saturating_add(RATE); + offset += RATE; } // Final block with multi-rate padding. let mut last = [0u8; RATE]; - let remaining = input.len().saturating_sub(offset); - if let Some(tail) = last.get_mut(..remaining) { - if let Some(src) = input.get(offset..) { - tail.copy_from_slice(src); - } - } - if let Some(b) = last.get_mut(remaining) { - *b ^= 0x01; - } - if let Some(b) = last.get_mut(RATE - 1) { - *b ^= 0x80; - } + let remaining = input.len() - offset; + last[..remaining].copy_from_slice(&input[offset..]); + last[remaining] ^= 0x01; + last[RATE - 1] ^= 0x80; absorb_block(&mut state, &last); permute(&mut state); // Squeeze the first 32 bytes (four lanes) as little-endian. let mut output = [0u8; 32]; for (i, lane) in state.iter().take(4).enumerate() { - let bytes = lane.to_le_bytes(); - let start = i.saturating_mul(8); - if let Some(dst) = output.get_mut(start..start.saturating_add(8)) { - dst.copy_from_slice(&bytes); - } + output[i * 8..i * 8 + 8].copy_from_slice(&lane.to_le_bytes()); } output } diff --git a/crypto/ethrex-crypto/src/tests/ecrecover_tests.rs b/crypto/ethrex-crypto/src/tests/ecrecover_tests.rs index ec3670d19..2cd69d95f 100644 --- a/crypto/ethrex-crypto/src/tests/ecrecover_tests.rs +++ b/crypto/ethrex-crypto/src/tests/ecrecover_tests.rs @@ -25,6 +25,15 @@ fn make_ecdsa_fixture(d: Scalar, kk: Scalar, msg: [u8; 32]) -> ([u8; 64], u8, [u let (rx, ry) = affine_xy(&r_point).expect("R is not identity"); let r = >::reduce_bytes(&rx.to_bytes()); assert!(!bool::from(r.is_zero()), "r must be nonzero"); + // rx is in Fp; since n < p, rx >= n with probability ~2^{-128}. When that + // happens r = rx-n and the signature requires the high-x recovery bit + // (recid >= 2, meaning R.x = r+n) which ecsm_ecrecover does not handle. + // Assert no reduction occurred so the low-x path is valid. + assert_eq!( + r.to_bytes(), + rx.to_bytes(), + "rx >= n: this kk needs high-x recovery (recid >= 2) — pick a different nonce" + ); // recid parity: low bit of Ry (big-endian, byte 31). let recid = ry.normalize().to_bytes()[31] & 1; diff --git a/crypto/ethrex-crypto/src/tests/ecsm_tests.rs b/crypto/ethrex-crypto/src/tests/ecsm_tests.rs index 08f5a20f6..ace1dc63a 100644 --- a/crypto/ethrex-crypto/src/tests/ecsm_tests.rs +++ b/crypto/ethrex-crypto/src/tests/ecsm_tests.rs @@ -83,3 +83,95 @@ fn cancelling_and_doubling_terms_fall_back() { assert!(lincomb2_with_oracle(&p, &k, &p, &k, soft_oracle).is_none()); assert!(lincomb2_with_oracle(&p, &k, &(-p), &k, soft_oracle).is_none()); } + +#[test] +fn k_half_n_minus_1_reconstructs_correctly() { + // k = (n-1)/2 satisfies k·P = -(k+1)·P for any P, so the oracle returns + // the same x-coordinate for both the k and k+1 calls (xa = xc). The + // solve_y algebra still holds: lambda² = 2·xa + xp = t, so the check + // passes and the correct ya is recovered. + let two_inv = Scalar::from(2u64) + .invert_vartime() + .expect("2 is invertible mod n"); + let k_half = -Scalar::ONE * two_inv; // (n-1)/2 + + let p1 = g_times(5); + let p2 = g_times(11); + let k2 = Scalar::from(99999u64); + + let expected = ProjectivePoint::lincomb(&p1, &k_half, &p2, &k2); + let got = lincomb2_with_oracle(&p1, &k_half, &p2, &k2, soft_oracle) + .expect("k=(n-1)/2 is not near-edge and must reconstruct correctly"); + assert_eq!(got.to_affine(), expected.to_affine()); +} + +#[test] +fn cross_point_cancellation_falls_back() { + // Construct k1, k2, P1 ≠ ±P2 such that k1·P1 = -(k2·P2), so + // k1·P1 + k2·P2 = O. The shared x-coordinate makes dxq = 0 → None. + // P1 = 3G, P2 = 7G: k1·3G = -k2·7G → k1 = -k2·7·3^{-1} mod n. + let p1 = g_times(3); + let p2 = g_times(7); + let k2 = Scalar::from(12345u64); + let three_inv = Scalar::from(3u64) + .invert_vartime() + .expect("3 is invertible mod n"); + let k1 = -(k2 * Scalar::from(7u64) * three_inv); + assert!( + lincomb2_with_oracle(&p1, &k1, &p2, &k2, soft_oracle).is_none(), + "cross-point cancellation (P1 ≠ ±P2, result = O) must fall back" + ); +} + +#[test] +fn solve_y_rejects_inconsistent_oracle_xc() { + // Directly test that solve_y's lambda² == t check fires when xc is wrong. + // This is the oracle-misbehavior guard: it cannot easily be reached via + // lincomb2_with_oracle because the oracle is Fn (no mutable state to + // return xa correct and xc wrong in separate calls). + let (xp, yp) = affine_xy(&g_times(3).to_affine()).unwrap(); + let k = Scalar::from(12345u64); + + let xa = soft_oracle(&xp, &k).unwrap(); + let xc_correct = soft_oracle(&xp, &(k + Scalar::ONE)).unwrap(); + // xc from k+100 is inconsistent with xa from k — lambda²=t must reject it. + let xc_wrong = soft_oracle(&xp, &(k + Scalar::from(100u64))).unwrap(); + + let dx = (xa - xp).normalize(); + let inv_den = Option::::from((yp.double() * dx).invert()) + .expect("dx is nonzero for k=12345"); + + assert!( + solve_y(&xp, &yp, &xa, &xc_correct, &dx, &inv_den).is_some(), + "correct xc must pass the lambda² check" + ); + assert!( + solve_y(&xp, &yp, &xa, &xc_wrong, &dx, &inv_den).is_none(), + "inconsistent xc (oracle misbehavior) must be rejected by the lambda² check" + ); +} + +#[test] +fn odd_y_base_point_reconstructs_correctly() { + // Validates the solve_y sign-selection argument: when P1 has odd y the + // reconstruction must still match ProjectivePoint::lincomb. + let (p1, _k_gen) = (2u64..200) + .find_map(|n| { + let p = g_times(n); + let (_, y) = affine_xy(&p.to_affine())?; + if y.normalize().to_bytes()[31] & 1 == 1 { + Some((p, n)) + } else { + None + } + }) + .expect("at least one of the first 200 multiples of G has odd y"); + + let p2 = g_times(13); + let k1 = Scalar::from(54321u64); + let k2 = Scalar::from(11111u64); + let expected = ProjectivePoint::lincomb(&p1, &k1, &p2, &k2); + let got = lincomb2_with_oracle(&p1, &k1, &p2, &k2, soft_oracle) + .expect("odd-y base point is non-degenerate and must reconstruct correctly"); + assert_eq!(got.to_affine(), expected.to_affine()); +} diff --git a/crypto/ethrex-crypto/src/tests/keccak_tests.rs b/crypto/ethrex-crypto/src/tests/keccak_tests.rs index efa526674..cde649fcb 100644 --- a/crypto/ethrex-crypto/src/tests/keccak_tests.rs +++ b/crypto/ethrex-crypto/src/tests/keccak_tests.rs @@ -11,6 +11,21 @@ fn check_keccak(input: &[u8]) { assert_eq!(got, want, "keccak256 mismatch for {}-byte input", input.len()); } +/// Cross-check our sponge against a hardcoded vector from the Ethereum spec. +fn check_keccak_kat(input: &[u8], expected_hex: &str) { + let expected: Vec = (0..expected_hex.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&expected_hex[i..i + 2], 16).unwrap()) + .collect(); + let got = keccak256_with_permute(input, keccak::f1600); + assert_eq!( + got.as_ref(), + expected.as_slice(), + "KAT mismatch for {}-byte input", + input.len() + ); +} + #[test] fn keccak_sponge_matches_trusted_permutation() { // Empty input. @@ -27,4 +42,32 @@ fn keccak_sponge_matches_trusted_permutation() { // Multi-block: ~1.5 × RATE (200 bytes), deterministic pattern. let long: Vec = (0u8..200).collect(); check_keccak(&long); + // 2 × RATE (272 bytes): two full absorb blocks + all-padding final block. + check_keccak(&[0xaa; 272]); + // 2 × RATE - 1 (271 bytes): two full absorbs + one-byte remainder. + check_keccak(&[0xbb; 271]); +} + +#[test] +fn keccak_sponge_known_answer_vectors() { + // Vectors from the Ethereum Yellow Paper / EIP-155. These use Keccak-256 + // (0x01 padding), NOT SHA3-256 (0x06 padding). Any sponge framing bug + // (wrong rate, wrong padding byte, wrong lane endianness) breaks these + // even if the differential test above passes. + + // keccak256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 + check_keccak_kat( + b"", + "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + ); + // keccak256("abc") + check_keccak_kat( + b"abc", + "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45", + ); + // keccak256("The quick brown fox jumps over the lazy dog") + check_keccak_kat( + b"The quick brown fox jumps over the lazy dog", + "4d741b6f1eb29cb2a9b9911c82f56fa8d73b04959d3d9d222895df6c0b28aa15", + ); } From 3e5b6211fbd9861a9e3f252d3e64d5410a070a33 Mon Sep 17 00:00:00 2001 From: jotabulacios Date: Wed, 24 Jun 2026 12:21:48 -0300 Subject: [PATCH 5/5] route ecrecover address hash through precompile --- crypto/ethrex-crypto/src/lib.rs | 18 +++++++++++++----- .../ethrex-crypto/src/tests/ecrecover_tests.rs | 9 ++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/crypto/ethrex-crypto/src/lib.rs b/crypto/ethrex-crypto/src/lib.rs index 76d24f045..980154e0f 100644 --- a/crypto/ethrex-crypto/src/lib.rs +++ b/crypto/ethrex-crypto/src/lib.rs @@ -43,7 +43,8 @@ impl Crypto for LambdaVmEcsmCrypto { recid: u8, msg: &[u8; 32], ) -> Result<[u8; 32], CryptoError> { - ecsm_ecrecover(sig, recid, msg) + let pk_bytes = ecsm_ecrecover(sig, recid, msg)?; + Ok(self.keccak256(&pk_bytes)) } fn keccak256(&self, input: &[u8]) -> [u8; 32] { @@ -59,10 +60,15 @@ impl Crypto for LambdaVmEcsmCrypto { // ── ECDSA secp256k1 recovery via the ECSM precompile ──────────────────────── -/// Recover the keccak hash of the uncompressed public key from a 64-byte +/// Recover the uncompressed public key bytes (X‖Y, 64 bytes) from a 64-byte /// signature, recovery id, and 32-byte message hash. Used by the ECRECOVER /// precompile (0x01). /// +/// Returns the raw 64-byte key; the caller is responsible for hashing it. +/// Keeping keccak out of this function lets `secp256k1_ecrecover` route the +/// hash through `self.keccak256`, which uses the keccak_permute precompile on +/// riscv64 instead of always falling back to software. +/// /// Mirrors the pure-Rust recovery in the `Crypto` trait default /// (`pk = r⁻¹·(s·R − z·G)`), but evaluates the 2-term linear combination /// `lincomb(G, u1, R, u2)` through the ECSM accelerator via [`ecsm_lincomb2`], @@ -71,7 +77,7 @@ impl Crypto for LambdaVmEcsmCrypto { /// We compute the recovery directly rather than calling k256's /// `recover_from_prehash`, which internally runs a *second* lincomb to /// re-verify the key — doubling the ECSM ecalls for no gain here. -fn ecsm_ecrecover(sig: &[u8; 64], recid: u8, msg: &[u8; 32]) -> Result<[u8; 32], CryptoError> { +fn ecsm_ecrecover(sig: &[u8; 64], recid: u8, msg: &[u8; 32]) -> Result<[u8; 64], CryptoError> { let r_bytes = <&FieldBytes>::from(&sig[..32]); let s_bytes = <&FieldBytes>::from(&sig[32..]); @@ -115,9 +121,11 @@ fn ecsm_ecrecover(sig: &[u8; 64], recid: u8, msg: &[u8; 32]) -> Result<[u8; 32], return Err(CryptoError::RecoveryFailed); } - // SEC1 uncompressed: 0x04 || X(32) || Y(32). The address is keccak(X || Y). + // SEC1 uncompressed: 0x04 || X(32) || Y(32). Return X‖Y for the caller to hash. let uncompressed = pk_affine.to_encoded_point(false); - Ok(keccak_hash(&uncompressed.as_bytes()[1..65])) + let mut pk_bytes = [0u8; 64]; + pk_bytes.copy_from_slice(&uncompressed.as_bytes()[1..65]); + Ok(pk_bytes) } /// ECSM-accelerated 2-term linear combination `k1·P1 + k2·P2`. diff --git a/crypto/ethrex-crypto/src/tests/ecrecover_tests.rs b/crypto/ethrex-crypto/src/tests/ecrecover_tests.rs index 2cd69d95f..f9c1d9242 100644 --- a/crypto/ethrex-crypto/src/tests/ecrecover_tests.rs +++ b/crypto/ethrex-crypto/src/tests/ecrecover_tests.rs @@ -93,7 +93,8 @@ fn ecrecover_known_answer_three_tuples() { let d = Scalar::from(d_u64); let kk = Scalar::from(kk_u64); let (sig, recid, expected) = make_ecdsa_fixture(d, kk, msg); - match ecsm_ecrecover(&sig, recid, &msg) { + let crypto = LambdaVmEcsmCrypto; + match crypto.secp256k1_ecrecover(&sig, recid, &msg) { Ok(got) => assert_eq!( got, expected, "ecrecover returned wrong address for d={d_u64:#x} kk={kk_u64:#x}" @@ -110,9 +111,10 @@ fn ecrecover_rejects_zero_s() { // r = 1 (nonzero, but s = 0 in the second half). sig[31] = 0x01; let msg = [0u8; 32]; + let crypto = LambdaVmEcsmCrypto; assert!( matches!( - ecsm_ecrecover(&sig, 0, &msg), + crypto.secp256k1_ecrecover(&sig, 0, &msg), Err(CryptoError::InvalidSignature) ), "expected InvalidSignature for zero s" @@ -125,9 +127,10 @@ fn ecrecover_rejects_zero_r() { let mut sig = [0u8; 64]; sig[63] = 0x01; // s = 1, r = 0 let msg = [0u8; 32]; + let crypto = LambdaVmEcsmCrypto; assert!( matches!( - ecsm_ecrecover(&sig, 0, &msg), + crypto.secp256k1_ecrecover(&sig, 0, &msg), Err(CryptoError::InvalidSignature) ), "expected InvalidSignature for zero r"