diff --git a/CLAUDE.md b/CLAUDE.md index b18ec747..6f5e0a56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ pub struct Config { - Don't import libraries for specific tests, import them at the top level instead ### Test Modification Policy -Treat existing tests as immutable. If an existing test fails or seems to need modification, ask for confirmation before changing it—the test may be catching a regression. This does not apply to new tests written as part of the current work, meaning the new content introduced in the current branch or PR. +Existing tests should be treated as correct unless they are genuinely broken or poorly scoped. If a test is truly broken (testing the wrong thing, asserting incorrect behavior, or not well scoped), fix or update it. If a test fails and you suspect it may be catching a real regression, ask for confirmation before changing it. This policy does not apply to new tests written as part of the current work. ### Project structure - Do not re-export dependencies from `crates/` in mod.rs or lib.rs files. Refer to the full path instead. diff --git a/Cargo.lock b/Cargo.lock index 933214d5..7460a0d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,9 +1313,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -2967,9 +2967,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures", ] @@ -3155,7 +3155,7 @@ dependencies = [ "metrics", "metrics-util 0.20.1", "quanta", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3293,9 +3293,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -4459,7 +4459,7 @@ checksum = "5e1aee7486406df3541b5a657204a11be97175a467d77bc98e6d94a66289fb80" dependencies = [ "arraydeque", "smallvec 2.0.0-alpha.12", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -5388,30 +5388,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", diff --git a/Cargo.toml b/Cargo.toml index 308147b7..e88ff1df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,9 @@ members = [ "tests/e2e/runner", "tests/e2e-gcs/runner", ] +exclude = [ + "contracts/stylus/attestation-verifier", +] [workspace.package] version = "0.1.0" diff --git a/contracts/stylus/attestation-verifier/Cargo.lock b/contracts/stylus/attestation-verifier/Cargo.lock new file mode 100644 index 00000000..d1532f4c --- /dev/null +++ b/contracts/stylus/attestation-verifier/Cargo.lock @@ -0,0 +1,2462 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloy-json-abi" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "125a1c373261b252e53e04d6e92c37d881833afc1315fceab53fd46045695640" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-primitives" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9485c56de23438127a731a6b4c87803d49faf1a7068dcd1d8768aca3a9edb9" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if 1.0.4", + "const-hex", + "derive_more", + "foldhash", + "hashbrown 0.15.5", + "indexmap", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.2", + "ruint", + "rustc-hash", + "serde", + "sha3", + "tiny-keccak", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" +dependencies = [ + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab81bab693da9bb79f7a95b64b394718259fdd7e41dceeced4cad57cb71c4f6a" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489f1620bb7e2483fb5819ed01ab6edc1d2f93939dce35a5695085a1afd1d699" +dependencies = [ + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap", + "proc-macro-error2", + "proc-macro2", + "quote", + "sha3", + "syn 2.0.117", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56cef806ad22d4392c5fc83cf8f2089f988eb99c7067b4e0c6f1971fc1cca318" +dependencies = [ + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "syn 2.0.117", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249" +dependencies = [ + "serde", + "winnow", +] + +[[package]] +name = "alloy-sol-types" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5383d34ea00079e6dd89c652bcbdb764db160cef84e6250926961a0b2295d04" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "serde", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "branches" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3cb31f305a2591edaae2206f29e1e05b19ba48eba41042a18735bcc0efe165" +dependencies = [ + "rustc_version 0.2.3", +] + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "const-hex" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9a108e542ddf1de36743a6126e94d6659dccda38fc8a77e80b915102ac784a" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "proptest", + "serde_core", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "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 = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if 1.0.4", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keccak-asm" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b646a74e746cd25045aa0fd42f4f7f78aa6d119380182c7e63a5593c4ab8df6f" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + +[[package]] +name = "keccak-const" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "mini-alloc" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9993556d3850cdbd0da06a3dc81297edcfa050048952d84d75e8b944e8f5af" +dependencies = [ + "cfg-if 1.0.4", + "wee_alloc", +] + +[[package]] +name = "mini-alloc" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b620761d4c29ede39bb1351c4360dba54d3b414d9fb460d37cba4c98b37a0b9d" +dependencies = [ + "cfg-if 1.0.4", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", + "serde", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", + "serde", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rclite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f528dfeba924f5fc67bb84a17fe043451d1b392758016ce2d9e9116649b0f35" +dependencies = [ + "branches", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "ruint" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11256b5fe8c68f56ac6f39ef0720e592f33d2367a4782740d9c9142e889c7fb4" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.5", + "rand 0.9.2", + "rlp", + "ruint-macro", + "serde", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.27", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser 0.7.0", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser 0.10.3", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sha3-asm" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b31139435f327c93c6038ed350ae4588e2c70a13d50599509fee6349967ba35a" +dependencies = [ + "cc", + "cfg-if 1.0.4", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "stylus-core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f164b490b2541dc58535c914cba9d0b11901b33cd39b9d86fe0da1234844823" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "cfg-if 1.0.4", + "dyn-clone", + "lazy_static", + "regex", +] + +[[package]] +name = "stylus-proc" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f69dcf078b332c89a3b7f36c9e49c7aa33be82f1bc93b97923df47816896cebc" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "cfg-if 1.0.4", + "convert_case 0.6.0", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "sha3", + "stylus-core", + "syn 2.0.117", + "syn-solidity", + "trybuild", +] + +[[package]] +name = "stylus-sdk" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29d4287eb49568e7f6d282f4ff8a3c800b0a9d9480cf5bb86236cfe55178609b" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "branches", + "cfg-if 1.0.4", + "clap", + "derivative", + "hex", + "keccak-const", + "mini-alloc 1.0.0", + "rclite", + "ruint", + "stylus-core", + "stylus-proc", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-solidity" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f425ae0b12e2f5ae65542e00898d500d4d318b4baf09f40fd0d410454e9947" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "synddb-stylus-attestation-verifier" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "mini-alloc 0.4.2", + "stylus-sdk", + "tokio", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "toml" +version = "1.0.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "trybuild" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c635f0191bd3a2941013e5062667100969f8c4e9cd787c14f977265d73616e" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "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.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver 1.0.27", +] + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[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.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/contracts/stylus/attestation-verifier/Cargo.toml b/contracts/stylus/attestation-verifier/Cargo.toml new file mode 100644 index 00000000..0631fb4a --- /dev/null +++ b/contracts/stylus/attestation-verifier/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "synddb-stylus-attestation-verifier" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Stylus attestation verifier for SyndDB TEE key registration" + +[dependencies] +alloy-primitives = "1.0" +alloy-sol-types = "1.0" +stylus-sdk = "0.10.0" +mini-alloc = "0.4" + +[dev-dependencies] +tokio = { version = "1", features = ["full"] } + +[features] +export-abi = ["stylus-sdk/export-abi"] + +[lib] +crate-type = ["lib", "cdylib"] diff --git a/contracts/stylus/attestation-verifier/rust-toolchain.toml b/contracts/stylus/attestation-verifier/rust-toolchain.toml new file mode 100644 index 00000000..e963dc45 --- /dev/null +++ b/contracts/stylus/attestation-verifier/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.87" +targets = ["wasm32-unknown-unknown"] diff --git a/contracts/stylus/attestation-verifier/src/lib.rs b/contracts/stylus/attestation-verifier/src/lib.rs new file mode 100644 index 00000000..f25ed4ac --- /dev/null +++ b/contracts/stylus/attestation-verifier/src/lib.rs @@ -0,0 +1,1663 @@ +// Only run this as a WASM if the export-abi feature is not set. +#![cfg_attr(not(any(feature = "export-abi", test)), no_main)] +extern crate alloc; + +use alloc::vec::Vec; +use alloy_primitives::{Address, FixedBytes, U64}; +use alloy_sol_types::{sol, SolValue}; +use stylus_sdk::{ + abi::Bytes, + call, + prelude::*, + storage::{StorageAddress, StorageBool, StorageFixedBytes, StorageMap, StorageU64}, +}; + +/// The ecrecover precompile address (0x01) +const ECRECOVER: Address = Address::with_last_byte(0x01); +/// The SHA-256 precompile address (0x02) +const SHA256_PRECOMPILE: Address = Address::with_last_byte(0x02); +/// The modular exponentiation precompile address (0x05) +const MODEXP_PRECOMPILE: Address = Address::with_last_byte(0x05); + +/// PKCS#1 v1.5 DigestInfo prefix for SHA-256 +/// DER encoding: SEQUENCE { SEQUENCE { OID sha256, NULL }, OCTET STRING (32 bytes) } +const PKCS1_SHA256_DIGEST_INFO: [u8; 19] = [ + 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, + 0x05, 0x00, 0x04, 0x20, +]; + +sol! { + /// ABI-encoded attestation claims. Must match the Solidity `PublicValuesStruct` + /// in `RiscZeroAttestationVerifier` for interface compatibility. + struct PublicValuesStruct { + bytes32 jwk_key_hash; + uint64 validity_window_start; + uint64 validity_window_end; + bytes32 image_digest_hash; + address tee_signing_key; + bool secboot; + bool dbgstat_disabled; + bytes32 audience_hash; + uint8 image_signature_v; + bytes32 image_signature_r; + bytes32 image_signature_s; + } + + /// Proof data containing the raw JWT and JWK RSA key material. + /// The caller provides the JWK key material; the contract verifies its hash + /// matches a stored trusted value before using it for RSA verification. + struct StylusProofData { + bytes jwt; + bytes jwk_modulus; + bytes jwk_exponent; + } + + event TrustedJwkAdded(bytes32 indexed kid_hash, bytes32 key_material_hash); + event TrustedJwkRemoved(bytes32 indexed kid_hash); + event AllowedImageDigestHashAdded(bytes32 indexed digest_hash); + event AllowedImageDigestHashRemoved(bytes32 indexed digest_hash); + event TrustedImageSignerAdded(address indexed signer); + event TrustedImageSignerRemoved(address indexed signer); + event OwnershipTransferred(address indexed previous_owner, address indexed new_owner); + + error NotOwner(address caller); + error AlreadyInitialized(); + error InvalidJwt(); + error UnsupportedAlgorithm(); + error UntrustedJwk(bytes32 kid_hash); + error JwkKeyMaterialMismatch(bytes32 kid_hash); + error RsaVerificationFailed(); + error InvalidIssuer(); + error ValidityWindowNotStarted(uint64 start, uint64 current); + error ValidityWindowExpired(uint64 end, uint64 current); + error ClaimsMismatch(); + error SecureBootRequired(); + error DebugModeNotAllowed(); + error UntrustedImageDigest(bytes32 digest_hash); + error UntrustedImageSigner(address signer); + error ImageSignatureInvalid(); + error EcrecoverFailed(); + error InvalidProofData(); + error InvalidPublicValues(); + error PrecompileFailed(); + error ZeroAddress(); + error ToleranceExceedsOneDay(); +} + +#[derive(SolidityError)] +pub enum VerifierError { + NotOwner(NotOwner), + AlreadyInitialized(AlreadyInitialized), + InvalidJwt(InvalidJwt), + UnsupportedAlgorithm(UnsupportedAlgorithm), + UntrustedJwk(UntrustedJwk), + JwkKeyMaterialMismatch(JwkKeyMaterialMismatch), + RsaVerificationFailed(RsaVerificationFailed), + InvalidIssuer(InvalidIssuer), + ValidityWindowNotStarted(ValidityWindowNotStarted), + ValidityWindowExpired(ValidityWindowExpired), + ClaimsMismatch(ClaimsMismatch), + SecureBootRequired(SecureBootRequired), + DebugModeNotAllowed(DebugModeNotAllowed), + UntrustedImageDigest(UntrustedImageDigest), + UntrustedImageSigner(UntrustedImageSigner), + ImageSignatureInvalid(ImageSignatureInvalid), + EcrecoverFailed(EcrecoverFailed), + InvalidProofData(InvalidProofData), + InvalidPublicValues(InvalidPublicValues), + PrecompileFailed(PrecompileFailed), + ZeroAddress(ZeroAddress), + ToleranceExceedsOneDay(ToleranceExceedsOneDay), +} + +/// Stylus attestation verifier that directly verifies GCP Confidential Space +/// JWT tokens on-chain using RSA signature verification via EVM precompiles. +/// +/// Unlike the `RiscZeroAttestationVerifier` which requires an off-chain zkVM proof, +/// this contract performs full JWT verification on-chain: +/// +/// 1. Caller provides the raw JWT token and JWK RSA public key material +/// 2. Contract verifies the JWK key material hash matches a stored trusted value +/// 3. Contract verifies the JWT RS256 signature using SHA-256 (0x02) and modexp (0x05) +/// 4. Contract parses and validates all attestation claims from the JWT payload +/// 5. Contract verifies the container image signature via ecrecover (0x01) +/// 6. Returns the TEE signing key address +/// +/// Implements the same `IAttestationVerifier` ABI interface as the Solidity verifier, +/// making it a drop-in replacement via `TeeKeyManager.updateAttestationVerifier()`. +#[storage] +#[entrypoint] +pub struct StylusAttestationVerifier { + owner: StorageAddress, + initialized: StorageBool, + expiration_tolerance: StorageU64, + /// Maps `keccak256(kid)` -> whether this JWK key ID is trusted + trusted_jwk_hashes: StorageMap, StorageBool>, + /// Maps `keccak256(kid)` -> `keccak256(modulus || exponent)` for key material verification. + /// When a caller provides JWK key material, the contract hashes it and compares against + /// the stored value to prevent callers from substituting their own RSA keys. + jwk_key_material_hashes: StorageMap, StorageFixedBytes<32>>, + allowed_image_digest_hashes: StorageMap, StorageBool>, + trusted_image_signers: StorageMap, +} + +#[public] +impl StylusAttestationVerifier { + /// Initializes the contract. Must be called once after deployment. + /// Sets the caller as the owner. Expiration tolerance is capped at 86400 seconds (1 day). + pub fn initialize(&mut self, expiration_tolerance: u64) -> Result<(), VerifierError> { + if self.initialized.get() { + return Err(VerifierError::AlreadyInitialized(AlreadyInitialized {})); + } + if expiration_tolerance > 86400 { + return Err(VerifierError::ToleranceExceedsOneDay( + ToleranceExceedsOneDay {}, + )); + } + self.owner.set(self.vm().msg_sender()); + self.expiration_tolerance.set(U64::from(expiration_tolerance)); + self.initialized.set(true); + Ok(()) + } + + /// Verifies a GCP Confidential Space JWT attestation directly on-chain. + /// + /// ABI-compatible with `IAttestationVerifier.verifyAttestationProof(bytes,bytes)`. + /// + /// # Arguments + /// * `public_values` - ABI-encoded `PublicValuesStruct` with claimed attestation values + /// * `proof_bytes` - ABI-encoded `StylusProofData` containing the raw JWT and JWK key material + /// + /// # Returns + /// The TEE signing key address from the verified attestation + pub fn verify_attestation_proof( + &self, + public_values: Bytes, + proof_bytes: Bytes, + ) -> Result { + // Decode inputs + let values = PublicValuesStruct::abi_decode(&public_values) + .map_err(|_| VerifierError::InvalidPublicValues(InvalidPublicValues {}))?; + let proof = StylusProofData::abi_decode(&proof_bytes) + .map_err(|_| VerifierError::InvalidProofData(InvalidProofData {}))?; + + // Parse JWT into header, payload, signature, and signing input + let jwt = parse_jwt(&proof.jwt) + .map_err(|_| VerifierError::InvalidJwt(InvalidJwt {}))?; + + // Verify algorithm is RS256 + let alg = json_get_string(&jwt.header_json, b"alg") + .ok_or(VerifierError::InvalidJwt(InvalidJwt {}))?; + if alg != b"RS256" { + return Err(VerifierError::UnsupportedAlgorithm(UnsupportedAlgorithm {})); + } + + // Extract kid and verify JWK is trusted + let kid = json_get_string(&jwt.header_json, b"kid") + .ok_or(VerifierError::InvalidJwt(InvalidJwt {}))?; + let kid_hash: FixedBytes<32> = self.vm().native_keccak256(kid); + + if !self.trusted_jwk_hashes.get(kid_hash) { + return Err(VerifierError::UntrustedJwk(UntrustedJwk { kid_hash })); + } + + // Verify the provided JWK key material matches the stored hash. + // This prevents callers from substituting their own RSA key to forge JWTs. + let mut key_material = + Vec::with_capacity(proof.jwk_modulus.len() + proof.jwk_exponent.len()); + key_material.extend_from_slice(&proof.jwk_modulus); + key_material.extend_from_slice(&proof.jwk_exponent); + let actual_key_hash: FixedBytes<32> = self.vm().native_keccak256(&key_material); + + let stored_key_hash = self.jwk_key_material_hashes.get(kid_hash); + if stored_key_hash != actual_key_hash { + return Err(VerifierError::JwkKeyMaterialMismatch( + JwkKeyMaterialMismatch { kid_hash }, + )); + } + + // Verify RS256 signature (RSA PKCS#1 v1.5 with SHA-256) + self.verify_rs256( + &jwt.signing_input, + &jwt.signature, + &proof.jwk_modulus, + &proof.jwk_exponent, + )?; + + // -- Validate JWT claims against public_values -- + + // Issuer must be Google's Confidential Computing service + let iss = json_get_string(&jwt.payload_json, b"iss") + .ok_or(VerifierError::InvalidJwt(InvalidJwt {}))?; + if iss != b"https://confidentialcomputing.googleapis.com" { + return Err(VerifierError::InvalidIssuer(InvalidIssuer {})); + } + + // Timestamps from JWT must match public_values + let iat = json_get_u64(&jwt.payload_json, b"iat") + .ok_or(VerifierError::InvalidJwt(InvalidJwt {}))?; + let exp = json_get_u64(&jwt.payload_json, b"exp") + .ok_or(VerifierError::InvalidJwt(InvalidJwt {}))?; + if iat != values.validity_window_start || exp != values.validity_window_end { + return Err(VerifierError::ClaimsMismatch(ClaimsMismatch {})); + } + + // Validate validity window against current block timestamp + let current_timestamp = self.vm().block_timestamp(); + if current_timestamp < values.validity_window_start { + return Err(VerifierError::ValidityWindowNotStarted( + ValidityWindowNotStarted { + start: values.validity_window_start, + current: current_timestamp, + }, + )); + } + + let tolerance: u64 = self.expiration_tolerance.get().to(); + if current_timestamp > values.validity_window_end + tolerance { + return Err(VerifierError::ValidityWindowExpired( + ValidityWindowExpired { + end: values.validity_window_end, + current: current_timestamp, + }, + )); + } + + // Audience hash must match + let aud = json_get_string(&jwt.payload_json, b"aud") + .ok_or(VerifierError::InvalidJwt(InvalidJwt {}))?; + let aud_hash: FixedBytes<32> = self.vm().native_keccak256(aud); + if aud_hash != values.audience_hash { + return Err(VerifierError::ClaimsMismatch(ClaimsMismatch {})); + } + + // Secure boot + let secboot = json_get_bool(&jwt.payload_json, b"secboot") + .ok_or(VerifierError::InvalidJwt(InvalidJwt {}))?; + if secboot != values.secboot { + return Err(VerifierError::ClaimsMismatch(ClaimsMismatch {})); + } + if !values.secboot { + return Err(VerifierError::SecureBootRequired(SecureBootRequired {})); + } + + // Debug status + let dbgstat = json_get_string(&jwt.payload_json, b"dbgstat"); + let dbgstat_disabled = dbgstat + .map(|d| d == b"disabled-since-boot") + .unwrap_or(false); + if dbgstat_disabled != values.dbgstat_disabled { + return Err(VerifierError::ClaimsMismatch(ClaimsMismatch {})); + } + if !values.dbgstat_disabled { + return Err(VerifierError::DebugModeNotAllowed(DebugModeNotAllowed {})); + } + + // Image digest hash: find `image_digest` in the JWT payload (nested under submods.container) + let image_digest = json_get_string(&jwt.payload_json, b"image_digest") + .ok_or(VerifierError::InvalidJwt(InvalidJwt {}))?; + let image_digest_hash: FixedBytes<32> = self.vm().native_keccak256(image_digest); + if image_digest_hash != values.image_digest_hash { + return Err(VerifierError::ClaimsMismatch(ClaimsMismatch {})); + } + if !self.allowed_image_digest_hashes.get(values.image_digest_hash) { + return Err(VerifierError::UntrustedImageDigest(UntrustedImageDigest { + digest_hash: values.image_digest_hash, + })); + } + + // JWK key hash must match + if kid_hash != values.jwk_key_hash { + return Err(VerifierError::ClaimsMismatch(ClaimsMismatch {})); + } + + // Verify image signature using ecrecover + let image_signer = self.ecrecover_call( + values.image_digest_hash, + values.image_signature_v, + values.image_signature_r, + values.image_signature_s, + )?; + + if image_signer == Address::ZERO { + return Err(VerifierError::ImageSignatureInvalid( + ImageSignatureInvalid {}, + )); + } + + if !self.trusted_image_signers.get(image_signer) { + return Err(VerifierError::UntrustedImageSigner(UntrustedImageSigner { + signer: image_signer, + })); + } + + Ok(values.tee_signing_key) + } + + // -- Admin functions -- + + /// Adds a trusted JWK key with its RSA key material hash. + /// + /// # Arguments + /// * `kid_hash` - `keccak256(kid)` where kid is the JWK Key ID from Google's JWKS + /// * `key_material_hash` - `keccak256(modulus_bytes || exponent_bytes)` where modulus and + /// exponent are the base64url-decoded RSA key components from the JWK + pub fn add_trusted_jwk( + &mut self, + kid_hash: FixedBytes<32>, + key_material_hash: FixedBytes<32>, + ) -> Result<(), VerifierError> { + self.only_owner()?; + self.trusted_jwk_hashes.setter(kid_hash).set(true); + self.jwk_key_material_hashes + .setter(kid_hash) + .set(key_material_hash); + self.vm().log(TrustedJwkAdded { + kid_hash, + key_material_hash, + }); + Ok(()) + } + + /// Removes a trusted JWK key. + pub fn remove_trusted_jwk(&mut self, kid_hash: FixedBytes<32>) -> Result<(), VerifierError> { + self.only_owner()?; + self.trusted_jwk_hashes.setter(kid_hash).set(false); + self.jwk_key_material_hashes + .setter(kid_hash) + .set(FixedBytes::ZERO); + self.vm().log(TrustedJwkRemoved { kid_hash }); + Ok(()) + } + + /// Adds an allowed container image digest hash. + pub fn add_allowed_image_digest_hash( + &mut self, + digest_hash: FixedBytes<32>, + ) -> Result<(), VerifierError> { + self.only_owner()?; + self.allowed_image_digest_hashes + .setter(digest_hash) + .set(true); + self.vm().log(AllowedImageDigestHashAdded { digest_hash }); + Ok(()) + } + + /// Removes an allowed container image digest hash. + pub fn remove_allowed_image_digest_hash( + &mut self, + digest_hash: FixedBytes<32>, + ) -> Result<(), VerifierError> { + self.only_owner()?; + self.allowed_image_digest_hashes + .setter(digest_hash) + .set(false); + self.vm().log(AllowedImageDigestHashRemoved { digest_hash }); + Ok(()) + } + + /// Adds a trusted image signer. + pub fn add_trusted_image_signer(&mut self, signer: Address) -> Result<(), VerifierError> { + self.only_owner()?; + self.trusted_image_signers.setter(signer).set(true); + self.vm().log(TrustedImageSignerAdded { signer }); + Ok(()) + } + + /// Removes a trusted image signer. + pub fn remove_trusted_image_signer(&mut self, signer: Address) -> Result<(), VerifierError> { + self.only_owner()?; + self.trusted_image_signers.setter(signer).set(false); + self.vm().log(TrustedImageSignerRemoved { signer }); + Ok(()) + } + + /// Transfers ownership of the contract. Reverts if `new_owner` is the zero address. + pub fn transfer_ownership(&mut self, new_owner: Address) -> Result<(), VerifierError> { + self.only_owner()?; + if new_owner == Address::ZERO { + return Err(VerifierError::ZeroAddress(ZeroAddress {})); + } + let previous_owner = self.owner.get(); + self.owner.set(new_owner); + self.vm().log(OwnershipTransferred { + previous_owner, + new_owner, + }); + Ok(()) + } + + // -- View functions -- + + pub fn owner(&self) -> Address { + self.owner.get() + } + + pub fn is_initialized(&self) -> bool { + self.initialized.get() + } + + pub fn expiration_tolerance(&self) -> U64 { + self.expiration_tolerance.get() + } + + pub fn is_jwk_trusted(&self, kid_hash: FixedBytes<32>) -> bool { + self.trusted_jwk_hashes.get(kid_hash) + } + + pub fn jwk_key_material_hash(&self, kid_hash: FixedBytes<32>) -> FixedBytes<32> { + self.jwk_key_material_hashes.get(kid_hash) + } + + pub fn is_image_digest_hash_allowed(&self, digest_hash: FixedBytes<32>) -> bool { + self.allowed_image_digest_hashes.get(digest_hash) + } + + pub fn is_image_signer_trusted(&self, signer: Address) -> bool { + self.trusted_image_signers.get(signer) + } +} + +// -- Internal methods -- + +impl StylusAttestationVerifier { + fn only_owner(&self) -> Result<(), VerifierError> { + let sender = self.vm().msg_sender(); + if sender != self.owner.get() { + return Err(VerifierError::NotOwner(NotOwner { caller: sender })); + } + Ok(()) + } + + /// Calls the ecrecover precompile (0x01) to recover an address from an ECDSA signature. + fn ecrecover_call( + &self, + hash: FixedBytes<32>, + v: u8, + r: FixedBytes<32>, + s: FixedBytes<32>, + ) -> Result { + let mut input = Vec::with_capacity(128); + input.extend_from_slice(hash.as_slice()); + let mut v_padded = [0u8; 32]; + v_padded[31] = v; + input.extend_from_slice(&v_padded); + input.extend_from_slice(r.as_slice()); + input.extend_from_slice(s.as_slice()); + + match call::static_call(self.vm(), Call::new(), ECRECOVER, &input) { + Ok(result) if result.len() >= 32 => Ok(Address::from_slice(&result[12..32])), + _ => Err(VerifierError::EcrecoverFailed(EcrecoverFailed {})), + } + } + + /// Calls the SHA-256 precompile (0x02). + fn sha256_call(&self, data: &[u8]) -> Result<[u8; 32], VerifierError> { + match call::static_call(self.vm(), Call::new(), SHA256_PRECOMPILE, data) { + Ok(result) if result.len() == 32 => { + let mut hash = [0u8; 32]; + hash.copy_from_slice(&result); + Ok(hash) + } + _ => Err(VerifierError::PrecompileFailed(PrecompileFailed {})), + } + } + + /// Calls the modexp precompile (0x05) to compute `base^exponent mod modulus`. + fn modexp_call( + &self, + base: &[u8], + exponent: &[u8], + modulus: &[u8], + ) -> Result, VerifierError> { + // Input format: base_length (32) || exp_length (32) || mod_length (32) || base || exp || mod + let mut input = Vec::with_capacity(96 + base.len() + exponent.len() + modulus.len()); + + let mut len_buf = [0u8; 32]; + len_buf[28..32].copy_from_slice(&(base.len() as u32).to_be_bytes()); + input.extend_from_slice(&len_buf); + + len_buf = [0u8; 32]; + len_buf[28..32].copy_from_slice(&(exponent.len() as u32).to_be_bytes()); + input.extend_from_slice(&len_buf); + + len_buf = [0u8; 32]; + len_buf[28..32].copy_from_slice(&(modulus.len() as u32).to_be_bytes()); + input.extend_from_slice(&len_buf); + + input.extend_from_slice(base); + input.extend_from_slice(exponent); + input.extend_from_slice(modulus); + + match call::static_call(self.vm(), Call::new(), MODEXP_PRECOMPILE, &input) { + Ok(result) => Ok(result), + Err(_) => Err(VerifierError::PrecompileFailed(PrecompileFailed {})), + } + } + + /// Verify an RS256 (RSA PKCS#1 v1.5 with SHA-256) signature. + fn verify_rs256( + &self, + signing_input: &[u8], + signature: &[u8], + modulus: &[u8], + exponent: &[u8], + ) -> Result<(), VerifierError> { + // 1. SHA-256 hash of the signing input (header_b64.payload_b64) + let hash = self.sha256_call(signing_input)?; + + // 2. RSA operation: signature^exponent mod modulus + let decrypted = self.modexp_call(signature, exponent, modulus)?; + + // 3. Verify PKCS#1 v1.5 padding matches expected hash + if !verify_pkcs1v15_sha256(&decrypted, &hash) { + return Err(VerifierError::RsaVerificationFailed( + RsaVerificationFailed {}, + )); + } + + Ok(()) + } +} + +// -- PKCS#1 v1.5 verification -- + +/// Verify PKCS#1 v1.5 padding for SHA-256. +/// +/// Expected format after RSA decryption: +/// `0x00 0x01 [0xFF padding] 0x00 [DigestInfo_SHA256] [32-byte hash]` +fn verify_pkcs1v15_sha256(decrypted: &[u8], expected_hash: &[u8; 32]) -> bool { + let len = decrypted.len(); + // Minimum: 2 (header) + 8 (min padding) + 1 (separator) + 19 (DigestInfo) + 32 (hash) = 62 + if len < 62 { + return false; + } + + // Check header bytes + if decrypted[0] != 0x00 || decrypted[1] != 0x01 { + return false; + } + + // Find separator (0x00) after 0xFF padding + let mut separator_pos = None; + for (i, &byte) in decrypted.iter().enumerate().skip(2) { + if byte == 0x00 { + separator_pos = Some(i); + break; + } + if byte != 0xFF { + return false; + } + } + + let separator_pos = match separator_pos { + Some(pos) => pos, + None => return false, + }; + + // PKCS#1 v1.5 requires at least 8 bytes of 0xFF padding + if separator_pos < 10 { + return false; + } + + // After separator: DigestInfo (19 bytes) + hash (32 bytes) = 51 bytes + let digest_start = separator_pos + 1; + if len - digest_start != PKCS1_SHA256_DIGEST_INFO.len() + 32 { + return false; + } + + // Verify DigestInfo prefix + if decrypted[digest_start..digest_start + PKCS1_SHA256_DIGEST_INFO.len()] + != PKCS1_SHA256_DIGEST_INFO + { + return false; + } + + // Verify hash + let hash_start = digest_start + PKCS1_SHA256_DIGEST_INFO.len(); + decrypted[hash_start..hash_start + 32] == *expected_hash +} + +// -- JWT parsing -- + +/// Parsed JWT components +struct ParsedJwt { + header_json: Vec, + payload_json: Vec, + signature: Vec, + /// The raw signing input: header_b64 + "." + payload_b64 + signing_input: Vec, +} + +/// Parse a JWT token into its components. +/// Splits on '.', base64url-decodes header and payload, and extracts the raw signature. +fn parse_jwt(jwt_bytes: &[u8]) -> Result { + let jwt_str = core::str::from_utf8(jwt_bytes).map_err(|_| "Invalid UTF-8")?; + + // Split into header.payload.signature + let mut parts = jwt_str.splitn(3, '.'); + let header_b64 = parts.next().ok_or("Missing header")?; + let payload_b64 = parts.next().ok_or("Missing payload")?; + let signature_b64 = parts.next().ok_or("Missing signature")?; + + let header_json = base64url_decode(header_b64.as_bytes())?; + let payload_json = base64url_decode(payload_b64.as_bytes())?; + let signature = base64url_decode(signature_b64.as_bytes())?; + + // Signing input is the raw base64url-encoded header.payload (not decoded) + let mut signing_input = Vec::with_capacity(header_b64.len() + 1 + payload_b64.len()); + signing_input.extend_from_slice(header_b64.as_bytes()); + signing_input.push(b'.'); + signing_input.extend_from_slice(payload_b64.as_bytes()); + + Ok(ParsedJwt { + header_json, + payload_json, + signature, + signing_input, + }) +} + +// -- Base64url decoding -- + +const BASE64_DECODE_TABLE: [i8; 128] = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, -1, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, + -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, + 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, +]; + +/// Decode base64url to bytes, handling missing padding. +fn base64url_decode(input: &[u8]) -> Result, &'static str> { + // Convert base64url to standard base64 + let mut standard = Vec::with_capacity(input.len() + 4); + for &c in input { + match c { + b'-' => standard.push(b'+'), + b'_' => standard.push(b'/'), + c => standard.push(c), + } + } + + // Add padding + match standard.len() % 4 { + 2 => { + standard.push(b'='); + standard.push(b'='); + } + 3 => standard.push(b'='), + _ => {} + } + + base64_decode(&standard) +} + +fn base64_decode(input: &[u8]) -> Result, &'static str> { + if input.len() % 4 != 0 { + return Err("Invalid base64 length"); + } + + let mut output = Vec::with_capacity(input.len() * 3 / 4); + let mut i = 0; + + while i < input.len() { + let a = input[i]; + let b = input[i + 1]; + let c = input[i + 2]; + let d = input[i + 3]; + + let va = if a < 128 { + BASE64_DECODE_TABLE[a as usize] + } else { + -1 + }; + let vb = if b < 128 { + BASE64_DECODE_TABLE[b as usize] + } else { + -1 + }; + let vc = if c < 128 { + BASE64_DECODE_TABLE[c as usize] + } else { + -1 + }; + let vd = if d < 128 { + BASE64_DECODE_TABLE[d as usize] + } else { + -1 + }; + + if va < 0 || vb < 0 { + return Err("Invalid base64 character"); + } + + output.push(((va as u8) << 2) | ((vb as u8) >> 4)); + + if c != b'=' { + if vc < 0 { + return Err("Invalid base64 character"); + } + output.push(((vb as u8) << 4) | ((vc as u8) >> 2)); + + if d != b'=' { + if vd < 0 { + return Err("Invalid base64 character"); + } + output.push(((vc as u8) << 6) | (vd as u8)); + } + } + + i += 4; + } + + Ok(output) +} + +// -- Minimal JSON extraction -- +// +// These functions extract specific claim values from JWT JSON payloads without +// a full JSON parser. They search for `"key":` patterns and parse the subsequent value. +// This is safe for GCP Confidential Space tokens where claim keys are unique. + +/// Find a JSON string value for a given key. +/// Returns the raw bytes between the quotes (no unescaping). +fn json_get_string<'a>(json: &'a [u8], key: &[u8]) -> Option<&'a [u8]> { + let after_colon = json_find_key(json, key)?; + + // Skip whitespace after colon + let mut i = after_colon; + while i < json.len() && is_json_whitespace(json[i]) { + i += 1; + } + + // Expect opening quote + if i >= json.len() || json[i] != b'"' { + return None; + } + i += 1; + let start = i; + + // Find closing quote (handle escaped quotes correctly, including \\") + while i < json.len() { + if json[i] == b'"' { + // Count consecutive backslashes before this quote. + // An even number means the quote is NOT escaped. + let mut backslash_count = 0; + let mut j = i; + while j > start && json[j - 1] == b'\\' { + backslash_count += 1; + j -= 1; + } + if backslash_count % 2 == 0 { + return Some(&json[start..i]); + } + } + i += 1; + } + + None +} + +/// Find a JSON unsigned integer value for a given key. +fn json_get_u64(json: &[u8], key: &[u8]) -> Option { + let after_colon = json_find_key(json, key)?; + + let mut i = after_colon; + while i < json.len() && is_json_whitespace(json[i]) { + i += 1; + } + + let mut result: u64 = 0; + let mut found = false; + while i < json.len() && json[i].is_ascii_digit() { + result = result.checked_mul(10)?.checked_add((json[i] - b'0') as u64)?; + i += 1; + found = true; + } + + if found { + Some(result) + } else { + None + } +} + +/// Find a JSON boolean value for a given key. +fn json_get_bool(json: &[u8], key: &[u8]) -> Option { + let after_colon = json_find_key(json, key)?; + + let mut i = after_colon; + while i < json.len() && is_json_whitespace(json[i]) { + i += 1; + } + + if json.len() >= i + 4 && &json[i..i + 4] == b"true" { + Some(true) + } else if json.len() >= i + 5 && &json[i..i + 5] == b"false" { + Some(false) + } else { + None + } +} + +/// Find the position immediately after the colon for a given JSON key. +/// Handles the case where a key appears as a substring of a value by verifying +/// the colon follows the closing quote of the key. +fn json_find_key(json: &[u8], key: &[u8]) -> Option { + // Build search pattern: "key" + let mut pattern = Vec::with_capacity(key.len() + 2); + pattern.push(b'"'); + pattern.extend_from_slice(key); + pattern.push(b'"'); + + let mut search_start = 0; + loop { + // Find next occurrence of "key" + let pos = find_subsequence(&json[search_start..], &pattern)?; + let abs_pos = search_start + pos; + let after_pattern = abs_pos + pattern.len(); + + // Skip whitespace and look for colon + let mut i = after_pattern; + while i < json.len() && is_json_whitespace(json[i]) { + i += 1; + } + + if i < json.len() && json[i] == b':' { + return Some(i + 1); + } + + // Not a key (probably inside a value), continue searching + search_start = abs_pos + 1; + if search_start >= json.len() { + return None; + } + } +} + +/// Find the first occurrence of `needle` in `haystack`. +fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option { + if needle.len() > haystack.len() { + return None; + } + haystack + .windows(needle.len()) + .position(|window| window == needle) +} + +fn is_json_whitespace(b: u8) -> bool { + b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_base64url_decode() { + let decoded = base64url_decode(b"SGVsbG8").unwrap(); + assert_eq!(&decoded, b"Hello"); + + let decoded = base64url_decode(b"PDw_Pz4-").unwrap(); + assert_eq!(&decoded, b"<>".as_slice()); + } + + #[test] + fn test_base64_decode_standard() { + let decoded = base64_decode(b"SGVsbG8gV29ybGQ=").unwrap(); + assert_eq!(&decoded, b"Hello World"); + } + + #[test] + fn test_json_get_string() { + let json = br#"{"iss":"https://confidentialcomputing.googleapis.com","aud":"test"}"#; + let iss = json_get_string(json, b"iss").unwrap(); + assert_eq!(iss, b"https://confidentialcomputing.googleapis.com"); + + let aud = json_get_string(json, b"aud").unwrap(); + assert_eq!(aud, b"test"); + } + + #[test] + fn test_json_get_string_with_spaces() { + let json = br#"{ "key" : "value" }"#; + let val = json_get_string(json, b"key").unwrap(); + assert_eq!(val, b"value"); + } + + #[test] + fn test_json_get_u64() { + let json = br#"{"iat":1764707757,"exp":1764711357}"#; + assert_eq!(json_get_u64(json, b"iat"), Some(1764707757)); + assert_eq!(json_get_u64(json, b"exp"), Some(1764711357)); + } + + #[test] + fn test_json_get_bool() { + let json = br#"{"secboot":true,"other":false}"#; + assert_eq!(json_get_bool(json, b"secboot"), Some(true)); + assert_eq!(json_get_bool(json, b"other"), Some(false)); + } + + #[test] + fn test_json_get_nested_image_digest() { + let json = br#"{"submods":{"container":{"image_digest":"sha256:61bb0cf00789","image_id":"sha256:daa1d4c16f8f"}}}"#; + let digest = json_get_string(json, b"image_digest").unwrap(); + assert_eq!(digest, b"sha256:61bb0cf00789"); + } + + #[test] + fn test_json_key_in_value_not_matched() { + // "iss" appears as a substring in the value of "data", but json_find_key should skip it + let json = br#"{"data":"missing","iss":"correct"}"#; + let iss = json_get_string(json, b"iss").unwrap(); + assert_eq!(iss, b"correct"); + } + + #[test] + fn test_parse_jwt_structure() { + // Create a minimal JWT: base64url("header") + "." + base64url("payload") + "." + base64url("sig") + let header = base64url_encode(br#"{"alg":"RS256","kid":"test-kid"}"#); + let payload = base64url_encode(br#"{"iss":"test","iat":1000,"exp":2000}"#); + let sig = base64url_encode(b"fake-signature"); + let jwt = format!("{}.{}.{}", header, payload, sig); + + let parsed = parse_jwt(jwt.as_bytes()).unwrap(); + assert_eq!( + json_get_string(&parsed.header_json, b"alg").unwrap(), + b"RS256" + ); + assert_eq!( + json_get_string(&parsed.header_json, b"kid").unwrap(), + b"test-kid" + ); + assert_eq!( + json_get_string(&parsed.payload_json, b"iss").unwrap(), + b"test" + ); + assert_eq!(json_get_u64(&parsed.payload_json, b"iat"), Some(1000)); + assert_eq!(json_get_u64(&parsed.payload_json, b"exp"), Some(2000)); + assert_eq!(parsed.signature, b"fake-signature"); + + // Signing input should be header_b64.payload_b64 + let expected_signing_input = format!("{}.{}", header, payload); + assert_eq!(parsed.signing_input, expected_signing_input.as_bytes()); + } + + /// Helper to build a PKCS#1 v1.5 padded message. + /// Format: 0x00 || 0x01 || [0xFF * ff_count] || 0x00 || digest_info || hash + fn build_pkcs1v15_padded(header: [u8; 2], ff_count: usize, hash: &[u8; 32]) -> Vec { + let mut padded = Vec::with_capacity(header.len() + ff_count + 1 + PKCS1_SHA256_DIGEST_INFO.len() + 32); + padded.extend_from_slice(&header); + padded.extend(core::iter::repeat_n(0xFF, ff_count)); + padded.push(0x00); + padded.extend_from_slice(&PKCS1_SHA256_DIGEST_INFO); + padded.extend_from_slice(hash); + padded + } + + #[test] + fn test_pkcs1v15_sha256_valid() { + // Build a valid PKCS#1 v1.5 padded message for a 256-byte RSA key + let hash = [0xAB; 32]; + // 256 - 2 - 1 - 19 - 32 = 202 bytes of 0xFF + let padded = build_pkcs1v15_padded([0x00, 0x01], 202, &hash); + assert_eq!(padded.len(), 256); + + assert!(verify_pkcs1v15_sha256(&padded, &hash)); + } + + #[test] + fn test_pkcs1v15_sha256_wrong_hash() { + let hash = [0xAB; 32]; + let wrong_hash = [0xCD; 32]; + let padded = build_pkcs1v15_padded([0x00, 0x01], 202, &hash); + + assert!(!verify_pkcs1v15_sha256(&padded, &wrong_hash)); + } + + #[test] + fn test_pkcs1v15_sha256_bad_header() { + let hash = [0xAB; 32]; + let padded = build_pkcs1v15_padded([0x00, 0x02], 202, &hash); // Wrong: should be 0x01 + + assert!(!verify_pkcs1v15_sha256(&padded, &hash)); + } + + #[test] + fn test_pkcs1v15_sha256_insufficient_padding() { + let hash = [0xAB; 32]; + // Only 3 bytes of 0xFF padding (minimum is 8) + let mut padded = vec![0x00, 0x01, 0xFF, 0xFF, 0xFF, 0x00]; + padded.extend_from_slice(&PKCS1_SHA256_DIGEST_INFO); + padded.extend_from_slice(&hash); + + assert!(!verify_pkcs1v15_sha256(&padded, &hash)); + } + + #[test] + fn test_public_values_abi_roundtrip() { + let values = PublicValuesStruct { + jwk_key_hash: FixedBytes::ZERO, + validity_window_start: 1000, + validity_window_end: 2000, + image_digest_hash: FixedBytes::ZERO, + tee_signing_key: Address::ZERO, + secboot: true, + dbgstat_disabled: true, + audience_hash: FixedBytes::ZERO, + image_signature_v: 27, + image_signature_r: FixedBytes::ZERO, + image_signature_s: FixedBytes::ZERO, + }; + + let encoded = values.abi_encode(); + let decoded = PublicValuesStruct::abi_decode(&encoded).unwrap(); + + assert_eq!(decoded.validity_window_start, 1000); + assert_eq!(decoded.validity_window_end, 2000); + assert!(decoded.secboot); + assert!(decoded.dbgstat_disabled); + assert_eq!(decoded.image_signature_v, 27); + } + + #[test] + fn test_stylus_proof_data_abi_roundtrip() { + let proof = StylusProofData { + jwt: alloy_primitives::Bytes::from_static(b"jwt-data"), + jwk_modulus: alloy_primitives::Bytes::from_static(b"modulus-data"), + jwk_exponent: alloy_primitives::Bytes::from_static(b"exponent-data"), + }; + + let encoded = proof.abi_encode(); + let decoded = StylusProofData::abi_decode(&encoded).unwrap(); + + assert_eq!(decoded.jwt.as_ref(), b"jwt-data"); + assert_eq!(decoded.jwk_modulus.as_ref(), b"modulus-data"); + assert_eq!(decoded.jwk_exponent.as_ref(), b"exponent-data"); + } + + // -- Base64 edge case tests -- + + #[test] + fn test_base64url_decode_empty() { + let decoded = base64url_decode(b"").unwrap(); + assert!(decoded.is_empty()); + } + + #[test] + fn test_base64url_decode_single_byte() { + // Single byte (0x41 = 'A') encodes to "QQ" in base64url (2 chars, needs == padding) + let decoded = base64url_decode(b"QQ").unwrap(); + assert_eq!(decoded, vec![0x41]); + } + + #[test] + fn test_base64url_decode_two_bytes() { + // Two bytes encode to 3 base64url chars (needs = padding) + let decoded = base64url_decode(b"QUI").unwrap(); + assert_eq!(decoded, vec![0x41, 0x42]); + } + + #[test] + fn test_base64url_decode_invalid_char() { + // Space is not a valid base64 character + assert!(base64url_decode(b"QQ Q").is_err()); + } + + #[test] + fn test_base64_decode_invalid_length() { + // Length not multiple of 4 after padding + assert!(base64_decode(b"QQQQ Q").is_err()); + } + + #[test] + fn test_base64url_decode_all_url_safe_chars() { + // Test that both - and _ are correctly translated + // "+" in standard base64 = "-" in base64url + // "/" in standard base64 = "_" in base64url + let decoded = base64url_decode(b"-_8").unwrap(); + // "-" -> "+", "_" -> "/", "8" stays; "+" = 62, "/" = 63, "8" = 60 + // After decoding: these are specific byte values + let standard_decoded = base64_decode(b"+/8=").unwrap(); + assert_eq!(decoded, standard_decoded); + } + + #[test] + fn test_base64_decode_high_byte_rejected() { + // Bytes >= 128 should be rejected + let input = [0x80, b'Q', b'Q', b'Q']; + assert!(base64_decode(&input).is_err()); + } + + // -- JSON extraction edge case tests -- + + #[test] + fn test_json_get_string_missing_key() { + let json = br#"{"iss":"test"}"#; + assert!(json_get_string(json, b"nonexistent").is_none()); + } + + #[test] + fn test_json_get_string_empty_value() { + let json = br#"{"key":""}"#; + let val = json_get_string(json, b"key").unwrap(); + assert_eq!(val, b""); + } + + #[test] + fn test_json_get_string_with_escaped_quote() { + // JSON: {"key":"value\"with\"quotes","other":"ok"} + // The value contains escaped quotes: value"with"quotes + let json = br#"{"key":"value\"with\"quotes","other":"ok"}"#; + let val = json_get_string(json, b"key").unwrap(); + // Raw bytes between the outer quotes include the backslash-quote sequences + assert_eq!(val, br#"value\"with\"quotes"#); + } + + #[test] + fn test_json_get_string_with_double_backslash_before_quote() { + // JSON: {"key":"value\\","other":"ok"} + // The \\\\ in the raw string is two backslashes in the actual bytes. + // The first backslash escapes the second, so the quote after them is the real closing quote. + let json = br#"{"key":"value\\","other":"ok"}"#; + let val = json_get_string(json, b"key").unwrap(); + assert_eq!(val, br#"value\\"#); + + // Verify the next key is still extractable + let other = json_get_string(json, b"other").unwrap(); + assert_eq!(other, b"ok"); + } + + #[test] + fn test_json_get_u64_missing_key() { + let json = br#"{"iat":1000}"#; + assert!(json_get_u64(json, b"exp").is_none()); + } + + #[test] + fn test_json_get_u64_zero() { + let json = br#"{"val":0}"#; + assert_eq!(json_get_u64(json, b"val"), Some(0)); + } + + #[test] + fn test_json_get_u64_max_safe() { + // Large but valid u64 value + let json = br#"{"big":18446744073709551615}"#; + assert_eq!(json_get_u64(json, b"big"), Some(u64::MAX)); + } + + #[test] + fn test_json_get_u64_overflow() { + // u64::MAX + 1 should overflow and return None + let json = br#"{"big":18446744073709551616}"#; + assert!(json_get_u64(json, b"big").is_none()); + } + + #[test] + fn test_json_get_u64_not_a_number() { + let json = br#"{"val":"not_a_number"}"#; + assert!(json_get_u64(json, b"val").is_none()); + } + + #[test] + fn test_json_get_bool_missing_key() { + let json = br#"{"secboot":true}"#; + assert!(json_get_bool(json, b"nonexistent").is_none()); + } + + #[test] + fn test_json_get_bool_not_a_bool() { + // Value is a string, not a bool literal + let json = br#"{"flag":"true"}"#; + assert!(json_get_bool(json, b"flag").is_none()); + } + + #[test] + fn test_json_get_string_key_at_end() { + let json = br#"{"first":"a","last":"z"}"#; + assert_eq!(json_get_string(json, b"last").unwrap(), b"z"); + } + + #[test] + fn test_json_get_string_value_contains_colon() { + let json = br#"{"url":"https://example.com:8080/path"}"#; + assert_eq!( + json_get_string(json, b"url").unwrap(), + b"https://example.com:8080/path" + ); + } + + #[test] + fn test_json_get_string_key_substring_of_another() { + // "aud" is a substring of "audience" - should only match exact key + let json = br#"{"audience":"wrong","aud":"correct"}"#; + assert_eq!(json_get_string(json, b"aud").unwrap(), b"correct"); + } + + #[test] + fn test_json_find_key_value_looks_like_key() { + // The value of "description" contains "iss": which looks like a key-value pair + let json = br#"{"description":"the iss is important","iss":"real_issuer"}"#; + let iss = json_get_string(json, b"iss").unwrap(); + assert_eq!(iss, b"real_issuer"); + } + + #[test] + fn test_json_get_nested_deeply() { + // Test deeply nested object extraction + let json = br#"{"a":{"b":{"c":{"target":"found"}}}}"#; + assert_eq!(json_get_string(json, b"target").unwrap(), b"found"); + } + + #[test] + fn test_json_get_u64_followed_by_comma() { + let json = br#"{"a":123,"b":456}"#; + assert_eq!(json_get_u64(json, b"a"), Some(123)); + assert_eq!(json_get_u64(json, b"b"), Some(456)); + } + + #[test] + fn test_json_get_u64_followed_by_brace() { + let json = br#"{"val":789}"#; + assert_eq!(json_get_u64(json, b"val"), Some(789)); + } + + // -- JWT parsing error tests -- + + #[test] + fn test_parse_jwt_invalid_utf8() { + let invalid = vec![0xFF, 0xFE, 0xFD]; + assert!(parse_jwt(&invalid).is_err()); + } + + #[test] + fn test_parse_jwt_no_dots() { + assert!(parse_jwt(b"nodots").is_err()); + } + + #[test] + fn test_parse_jwt_one_dot() { + assert!(parse_jwt(b"only.one").is_err()); + } + + #[test] + fn test_parse_jwt_invalid_base64_in_header() { + // "!!!" is not valid base64 + assert!(parse_jwt(b"!!!.cGF5bG9hZA.c2ln").is_err()); + } + + #[test] + fn test_parse_jwt_empty_parts() { + // Three dots but empty segments - base64url_decode of "" returns empty vec, which is valid + let result = parse_jwt(b".."); + // Empty header/payload decode to empty JSON, which is fine at the parsing level + assert!(result.is_ok()); + } + + #[test] + fn test_parse_jwt_extra_dots_ignored() { + // JWT with extra dots - splitn(3, '.') means the third part includes everything after second dot + let header = base64url_encode(br#"{"alg":"RS256"}"#); + let payload = base64url_encode(br#"{"iss":"test"}"#); + let jwt = format!("{}.{}.sig.extra.dots", header, payload); + // The signature part will be "sig.extra.dots" which will fail base64 decode + // because '.' is not valid base64 + assert!(parse_jwt(jwt.as_bytes()).is_err()); + } + + #[test] + fn test_parse_jwt_preserves_signing_input_exactly() { + // The signing input must be the exact base64url-encoded header.payload + let header_b64 = base64url_encode(br#"{"alg":"RS256","typ":"JWT"}"#); + let payload_b64 = base64url_encode(br#"{"sub":"test"}"#); + let sig_b64 = base64url_encode(b"\x01\x02\x03"); + let jwt = format!("{}.{}.{}", header_b64, payload_b64, sig_b64); + + let parsed = parse_jwt(jwt.as_bytes()).unwrap(); + let expected = format!("{}.{}", header_b64, payload_b64); + assert_eq!( + core::str::from_utf8(&parsed.signing_input).unwrap(), + &expected + ); + } + + // -- PKCS#1 v1.5 additional edge case tests -- + + #[test] + fn test_pkcs1v15_sha256_non_ff_in_padding() { + // A non-0xFF byte (0xFE) in the padding should fail + let hash = [0xAB; 32]; + let mut padded = vec![0x00, 0x01]; + padded.extend(core::iter::repeat_n(0xFF, 100)); + padded.push(0xFE); // Invalid: not 0xFF + padded.extend(core::iter::repeat_n(0xFF, 101)); + padded.push(0x00); + padded.extend_from_slice(&PKCS1_SHA256_DIGEST_INFO); + padded.extend_from_slice(&hash); + + assert!(!verify_pkcs1v15_sha256(&padded, &hash)); + } + + #[test] + fn test_pkcs1v15_sha256_wrong_digest_info() { + let hash = [0xAB; 32]; + let mut padded = vec![0x00, 0x01]; + padded.extend(core::iter::repeat_n(0xFF, 202)); + padded.push(0x00); + // Wrong DigestInfo - change first byte + let mut wrong_digest_info = PKCS1_SHA256_DIGEST_INFO; + wrong_digest_info[0] = 0x31; // Should be 0x30 + padded.extend_from_slice(&wrong_digest_info); + padded.extend_from_slice(&hash); + + assert!(!verify_pkcs1v15_sha256(&padded, &hash)); + } + + #[test] + fn test_pkcs1v15_sha256_too_short() { + // Less than 62 bytes should fail immediately + let hash = [0xAB; 32]; + let padded = vec![0x00, 0x01, 0xFF, 0x00]; // Way too short + assert!(!verify_pkcs1v15_sha256(&padded, &hash)); + } + + #[test] + fn test_pkcs1v15_sha256_minimum_valid_padding() { + // Exactly 8 bytes of 0xFF padding (minimum per PKCS#1 v1.5 spec) + let hash = [0xAB; 32]; + let padded = build_pkcs1v15_padded([0x00, 0x01], 8, &hash); + // 2 + 8 + 1 + 19 + 32 = 62 bytes (minimum valid) + assert_eq!(padded.len(), 62); + + assert!(verify_pkcs1v15_sha256(&padded, &hash)); + } + + #[test] + fn test_pkcs1v15_sha256_128_byte_key() { + // 1024-bit RSA key (128 bytes) - valid but smaller padding + let hash = [0xAB; 32]; + // 128 - 2 - 1 - 19 - 32 = 74 bytes of 0xFF + let padded = build_pkcs1v15_padded([0x00, 0x01], 74, &hash); + assert_eq!(padded.len(), 128); + + assert!(verify_pkcs1v15_sha256(&padded, &hash)); + } + + #[test] + fn test_pkcs1v15_sha256_no_separator() { + // All 0xFF with no 0x00 separator - should fail + let hash = [0xAB; 32]; + let mut padded = vec![0x00, 0x01]; + padded.extend(core::iter::repeat_n(0xFF, 256)); + // No separator, no DigestInfo, no hash + assert!(!verify_pkcs1v15_sha256(&padded, &hash)); + } + + #[test] + fn test_pkcs1v15_sha256_extra_data_after_hash() { + // Correct padding but extra bytes after the hash (wrong total length) + let hash = [0xAB; 32]; + let mut padded = build_pkcs1v15_padded([0x00, 0x01], 202, &hash); + padded.push(0x00); // Extra byte - makes total length wrong + assert_eq!(padded.len(), 257); + + // The check `len - digest_start != 51` should catch this + assert!(!verify_pkcs1v15_sha256(&padded, &hash)); + } + + // -- Realistic GCP Confidential Space JWT parsing test -- + + #[test] + fn test_parse_realistic_gcp_jwt() { + // Build a realistic GCP Confidential Space JWT payload + let header = br#"{"alg":"RS256","kid":"1a2b3c4d5e6f","typ":"JWT"}"#; + let payload = br#"{"iss":"https://confidentialcomputing.googleapis.com","sub":"https://www.googleapis.com/compute/v1/projects/my-project/zones/us-central1-a/instances/12345","aud":"https://synddb.example.com","iat":1764707757,"exp":1764711357,"nbf":1764707757,"secboot":true,"hwmodel":"GCP_AMD_SEV","swname":"CONFIDENTIAL_SPACE","dbgstat":"disabled-since-boot","submods":{"container":{"image_digest":"sha256:61bb0cf00789abcdef1234567890abcdef1234567890abcdef1234567890abcd","image_reference":"us-central1-docker.pkg.dev/my-project/my-repo/sequencer@sha256:61bb0cf00789abcdef1234567890abcdef1234567890abcdef1234567890abcd"}}}"#; + let sig = b"fake-rsa-signature-for-testing"; + + let header_b64 = base64url_encode(header); + let payload_b64 = base64url_encode(payload); + let sig_b64 = base64url_encode(sig); + let jwt = format!("{}.{}.{}", header_b64, payload_b64, sig_b64); + + let parsed = parse_jwt(jwt.as_bytes()).unwrap(); + + // Verify header fields + assert_eq!( + json_get_string(&parsed.header_json, b"alg").unwrap(), + b"RS256" + ); + assert_eq!( + json_get_string(&parsed.header_json, b"kid").unwrap(), + b"1a2b3c4d5e6f" + ); + assert_eq!( + json_get_string(&parsed.header_json, b"typ").unwrap(), + b"JWT" + ); + + // Verify payload claims + assert_eq!( + json_get_string(&parsed.payload_json, b"iss").unwrap(), + b"https://confidentialcomputing.googleapis.com" + ); + assert_eq!( + json_get_string(&parsed.payload_json, b"aud").unwrap(), + b"https://synddb.example.com" + ); + assert_eq!(json_get_u64(&parsed.payload_json, b"iat"), Some(1764707757)); + assert_eq!(json_get_u64(&parsed.payload_json, b"exp"), Some(1764711357)); + assert_eq!(json_get_u64(&parsed.payload_json, b"nbf"), Some(1764707757)); + assert_eq!(json_get_bool(&parsed.payload_json, b"secboot"), Some(true)); + assert_eq!( + json_get_string(&parsed.payload_json, b"hwmodel").unwrap(), + b"GCP_AMD_SEV" + ); + assert_eq!( + json_get_string(&parsed.payload_json, b"swname").unwrap(), + b"CONFIDENTIAL_SPACE" + ); + assert_eq!( + json_get_string(&parsed.payload_json, b"dbgstat").unwrap(), + b"disabled-since-boot" + ); + assert_eq!( + json_get_string(&parsed.payload_json, b"image_digest").unwrap(), + b"sha256:61bb0cf00789abcdef1234567890abcdef1234567890abcdef1234567890abcd" + ); + + // Verify signature was decoded + assert_eq!(parsed.signature, sig); + } + + // -- Modexp input format verification -- + + #[test] + fn test_modexp_input_construction() { + // Verify the modexp input format matches the EIP-198 spec: + // base_length (32 bytes) || exp_length (32 bytes) || mod_length (32 bytes) || base || exp || mod + let base = vec![0x01; 256]; // 256-byte signature + let exponent = vec![0x01, 0x00, 0x01]; // Common RSA exponent (65537) + let modulus = vec![0x02; 256]; // 256-byte modulus + + // Reconstruct what modexp_call would build (without calling the precompile) + let mut input = Vec::with_capacity(96 + base.len() + exponent.len() + modulus.len()); + + let mut len_buf = [0u8; 32]; + len_buf[28..32].copy_from_slice(&(base.len() as u32).to_be_bytes()); + input.extend_from_slice(&len_buf); + + len_buf = [0u8; 32]; + len_buf[28..32].copy_from_slice(&(exponent.len() as u32).to_be_bytes()); + input.extend_from_slice(&len_buf); + + len_buf = [0u8; 32]; + len_buf[28..32].copy_from_slice(&(modulus.len() as u32).to_be_bytes()); + input.extend_from_slice(&len_buf); + + input.extend_from_slice(&base); + input.extend_from_slice(&exponent); + input.extend_from_slice(&modulus); + + // Verify total length: 96 (headers) + 256 (base) + 3 (exp) + 256 (mod) = 611 + assert_eq!(input.len(), 611); + + // Verify base_length = 256 (big-endian in last 4 bytes of first 32-byte word) + assert_eq!(input[28..32], [0x00, 0x00, 0x01, 0x00]); + + // Verify exp_length = 3 + assert_eq!(input[60..64], [0x00, 0x00, 0x00, 0x03]); + + // Verify mod_length = 256 + assert_eq!(input[92..96], [0x00, 0x00, 0x01, 0x00]); + + // Verify base starts at offset 96 + assert_eq!(&input[96..98], &[0x01, 0x01]); + + // Verify exponent starts at offset 96 + 256 = 352 + assert_eq!(&input[352..355], &[0x01, 0x00, 0x01]); + + // Verify modulus starts at offset 352 + 3 = 355 + assert_eq!(&input[355..357], &[0x02, 0x02]); + } + + // -- ecrecover input format verification -- + + #[test] + fn test_ecrecover_input_construction() { + // Verify the ecrecover input format: hash (32) || v (32, right-padded) || r (32) || s (32) + let hash = FixedBytes::<32>::from([0xAA; 32]); + let v: u8 = 28; + let r = FixedBytes::<32>::from([0xBB; 32]); + let s = FixedBytes::<32>::from([0xCC; 32]); + + // Reconstruct what ecrecover_call would build + let mut input = Vec::with_capacity(128); + input.extend_from_slice(hash.as_slice()); + let mut v_padded = [0u8; 32]; + v_padded[31] = v; + input.extend_from_slice(&v_padded); + input.extend_from_slice(r.as_slice()); + input.extend_from_slice(s.as_slice()); + + assert_eq!(input.len(), 128); + + // Hash at bytes 0..32 + assert_eq!(&input[0..32], &[0xAA; 32]); + + // V at byte 63 (right-aligned in 32-byte word) + assert_eq!(input[31 + 1..63], [0u8; 31]); + assert_eq!(input[63], 28); + + // R at bytes 64..96 + assert_eq!(&input[64..96], &[0xBB; 32]); + + // S at bytes 96..128 + assert_eq!(&input[96..128], &[0xCC; 32]); + } + + // -- ABI encoding with non-zero values -- + + #[test] + fn test_public_values_abi_roundtrip_with_real_values() { + let values = PublicValuesStruct { + jwk_key_hash: FixedBytes::from([0x11; 32]), + validity_window_start: 1764707757, + validity_window_end: 1764711357, + image_digest_hash: FixedBytes::from([0x22; 32]), + tee_signing_key: Address::from([0x33; 20]), + secboot: true, + dbgstat_disabled: true, + audience_hash: FixedBytes::from([0x44; 32]), + image_signature_v: 28, + image_signature_r: FixedBytes::from([0x55; 32]), + image_signature_s: FixedBytes::from([0x66; 32]), + }; + + let encoded = values.abi_encode(); + let decoded = PublicValuesStruct::abi_decode(&encoded).unwrap(); + + assert_eq!(decoded.jwk_key_hash, FixedBytes::from([0x11; 32])); + assert_eq!(decoded.validity_window_start, 1764707757); + assert_eq!(decoded.validity_window_end, 1764711357); + assert_eq!(decoded.image_digest_hash, FixedBytes::from([0x22; 32])); + assert_eq!(decoded.tee_signing_key, Address::from([0x33; 20])); + assert!(decoded.secboot); + assert!(decoded.dbgstat_disabled); + assert_eq!(decoded.audience_hash, FixedBytes::from([0x44; 32])); + assert_eq!(decoded.image_signature_v, 28); + assert_eq!(decoded.image_signature_r, FixedBytes::from([0x55; 32])); + assert_eq!(decoded.image_signature_s, FixedBytes::from([0x66; 32])); + } + + #[test] + fn test_public_values_abi_decode_invalid_data() { + // Too short to be valid ABI-encoded data + assert!(PublicValuesStruct::abi_decode(&[0u8; 10]).is_err()); + } + + #[test] + fn test_stylus_proof_data_abi_decode_invalid_data() { + assert!(StylusProofData::abi_decode(&[0u8; 10]).is_err()); + } + + // -- find_subsequence tests -- + + #[test] + fn test_find_subsequence_found() { + assert_eq!(find_subsequence(b"hello world", b"world"), Some(6)); + } + + #[test] + fn test_find_subsequence_not_found() { + assert_eq!(find_subsequence(b"hello world", b"xyz"), None); + } + + #[test] + fn test_find_subsequence_needle_larger_than_haystack() { + assert_eq!(find_subsequence(b"hi", b"hello"), None); + } + + #[test] + #[should_panic(expected = "window size must be non-zero")] + fn test_find_subsequence_empty_needle_panics() { + // Empty needle causes a panic via .windows(0) - this is fine since + // json_find_key always builds patterns of at least 3 bytes ("key") + let _ = find_subsequence(b"hello", b""); + } + + #[test] + fn test_find_subsequence_at_start() { + assert_eq!(find_subsequence(b"hello", b"hel"), Some(0)); + } + + #[test] + fn test_find_subsequence_at_end() { + assert_eq!(find_subsequence(b"hello", b"llo"), Some(2)); + } + + /// Helper for tests: encode bytes to base64url (no padding) + fn base64url_encode(input: &[u8]) -> String { + const TABLE: &[u8] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + let mut result = String::with_capacity(input.len().div_ceil(3) * 4); + for chunk in input.chunks(3) { + let b0 = chunk[0] as usize; + let b1 = if chunk.len() > 1 { chunk[1] as usize } else { 0 }; + let b2 = if chunk.len() > 2 { chunk[2] as usize } else { 0 }; + + result.push(TABLE[(b0 >> 2) & 0x3F] as char); + result.push(TABLE[((b0 << 4) | (b1 >> 4)) & 0x3F] as char); + if chunk.len() > 1 { + result.push(TABLE[((b1 << 2) | (b2 >> 6)) & 0x3F] as char); + } + if chunk.len() > 2 { + result.push(TABLE[b2 & 0x3F] as char); + } + } + result + } +} diff --git a/crates/synddb-bootstrap/Cargo.toml b/crates/synddb-bootstrap/Cargo.toml index 5b7956bc..1123fd51 100644 --- a/crates/synddb-bootstrap/Cargo.toml +++ b/crates/synddb-bootstrap/Cargo.toml @@ -29,6 +29,7 @@ url = { workspace = true } # Serialization serde = { workspace = true } +serde_json = { workspace = true } hex = { workspace = true } # Blockchain / Contract interaction @@ -37,6 +38,7 @@ alloy = { workspace = true, features = [ "signer-local", "provider-http", "network", + "sol-types", ] } # CLI configuration @@ -53,4 +55,3 @@ tracing = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["rt", "macros"] } -serde_json = { workspace = true } diff --git a/crates/synddb-bootstrap/src/config.rs b/crates/synddb-bootstrap/src/config.rs index f4306e1d..15e40679 100644 --- a/crates/synddb-bootstrap/src/config.rs +++ b/crates/synddb-bootstrap/src/config.rs @@ -82,6 +82,15 @@ pub struct BootstrapConfig { /// image digest string (e.g., "sha256:abc123...") using an Ethereum key. #[arg(long, env = "IMAGE_SIGNATURE")] pub image_signature: Option, + + /// Google JWKS URL for fetching RSA public keys used to verify Confidential Space JWTs. + /// Only used when `prover_mode=stylus`. Defaults to the GCP Confidential Space JWKS endpoint. + #[arg( + long, + env = "GOOGLE_JWKS_URL", + default_value = "https://www.googleapis.com/service_accounts/v1/metadata/jwk/signer@confidentialspace-sign.iam.gserviceaccount.com" + )] + pub google_jwks_url: String, } impl Default for BootstrapConfig { @@ -103,6 +112,7 @@ impl Default for BootstrapConfig { verification_max_retries: 5, relayer_timeout: Duration::from_secs(180), image_signature: None, + google_jwks_url: "https://www.googleapis.com/service_accounts/v1/metadata/jwk/signer@confidentialspace-sign.iam.gserviceaccount.com".into(), } } } @@ -159,11 +169,15 @@ impl BootstrapConfig { #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ValueEnum)] #[serde(rename_all = "lowercase")] pub enum ProverMode { - /// Use self-hosted proof service + /// Use self-hosted RISC Zero proof service #[default] Service, /// Use mock prover for testing (no real proof) Mock, + /// Use Stylus on-chain verification (skips RISC Zero zkVM). + /// The Stylus contract verifies the GCP Confidential Space JWT directly + /// on-chain using RSA signature verification via EVM precompiles. + Stylus, } #[cfg(test)] @@ -204,4 +218,19 @@ mod tests { }; assert!(config.validate().is_err()); } + + #[test] + fn test_validate_stylus_valid() { + let config = BootstrapConfig { + enable_key_bootstrap: true, + bridge_address: Some("0x1234567890123456789012345678901234567890".into()), + rpc_url: Some("http://localhost:8545".into()), + chain_id: Some(1), + relayer_url: Some("http://localhost:3000".into()), + prover_mode: ProverMode::Stylus, + image_signature: Some("0x".to_string() + &"ab".repeat(65)), + ..Default::default() + }; + assert!(config.validate().is_ok()); + } } diff --git a/crates/synddb-bootstrap/src/lib.rs b/crates/synddb-bootstrap/src/lib.rs index 864bc2aa..a8d38e7b 100644 --- a/crates/synddb-bootstrap/src/lib.rs +++ b/crates/synddb-bootstrap/src/lib.rs @@ -5,11 +5,21 @@ //! //! 1. Generate ephemeral signing key inside TEE //! 2. Fetch attestation token from Confidential Space -//! 3. Request RISC Zero proof from proof service +//! 3. Generate attestation proof (RISC Zero service or Stylus local construction) //! 4. Sign registration request (EIP-712) //! 5. Send to relayer for on-chain submission (relayer pays gas) //! 6. Verify key registration on-chain //! +//! # Verification Modes +//! +//! - **Service** (default): Uses RISC Zero zkVM proof service for ZK proof generation. +//! The proof service runs the RISC Zero guest program to verify the JWT and generate +//! a Groth16 proof, which is verified on-chain by `RiscZeroAttestationVerifier`. +//! - **Stylus**: Constructs proof data locally and sends the raw JWT for direct on-chain +//! verification by an Arbitrum Stylus contract. The contract verifies the RS256 signature +//! using SHA-256 and modexp EVM precompiles. No external proof service needed. +//! - **Mock**: For testing only, generates invalid proofs +//! //! # Usage //! //! ```ignore diff --git a/crates/synddb-bootstrap/src/proof_client.rs b/crates/synddb-bootstrap/src/proof_client.rs index c9f5f087..73f5c197 100644 --- a/crates/synddb-bootstrap/src/proof_client.rs +++ b/crates/synddb-bootstrap/src/proof_client.rs @@ -1,7 +1,11 @@ -//! Client for the GPU proof generation service +//! Client for proof generation (RISC Zero service or Stylus local construction) use crate::{BootstrapConfig, BootstrapError, ProverMode}; -use alloy::primitives::Address; +use alloy::{ + primitives::{keccak256, Address, FixedBytes}, + sol, + sol_types::SolValue, +}; use serde::{Deserialize, Serialize}; use std::time::Duration; use tracing::{debug, info, warn}; @@ -9,6 +13,33 @@ use tracing::{debug, info, warn}; /// Timeout for GCP metadata requests const METADATA_TIMEOUT: Duration = Duration::from_secs(5); +/// Timeout for JWKS fetch requests +const JWKS_FETCH_TIMEOUT: Duration = Duration::from_secs(10); + +sol! { + /// ABI-encoded attestation claims. Must match the Solidity/Stylus definitions. + struct PublicValuesStruct { + bytes32 jwk_key_hash; + uint64 validity_window_start; + uint64 validity_window_end; + bytes32 image_digest_hash; + address tee_signing_key; + bool secboot; + bool dbgstat_disabled; + bytes32 audience_hash; + uint8 image_signature_v; + bytes32 image_signature_r; + bytes32 image_signature_s; + } + + /// Proof data for the Stylus verifier: raw JWT + JWK RSA key material. + struct StylusProofData { + bytes jwt; + bytes jwk_modulus; + bytes jwk_exponent; + } +} + /// Fetch an identity token from the GCP metadata server for authenticating to Cloud Run. /// Uses the provided HTTP client to avoid creating new connections for each request. async fn fetch_identity_token( @@ -64,7 +95,7 @@ struct ProofRequest { pub struct ProofResponse { /// ABI-encoded `PublicValuesStruct` (hex) pub public_values: String, - /// RISC Zero proof bytes (hex) + /// Proof bytes (hex) - RISC Zero Groth16 proof or ABI-encoded `StylusProofData` pub proof_bytes: String, /// Derived TEE address for verification pub tee_address: String, @@ -77,6 +108,8 @@ pub struct ProofClient { service_url: String, timeout: Duration, health_check_timeout: Duration, + prover_mode: ProverMode, + google_jwks_url: String, } impl ProofClient { @@ -88,6 +121,8 @@ impl ProofClient { .clone() .ok_or_else(|| BootstrapError::Config("PROOF_SERVICE_URL is required".into()))?, ProverMode::Mock => "mock://localhost".into(), + // Stylus mode constructs proofs locally; no external service needed + ProverMode::Stylus => String::new(), }; let client = reqwest::Client::builder() @@ -100,6 +135,8 @@ impl ProofClient { service_url, timeout: config.proof_timeout, health_check_timeout: config.proof_health_check_timeout, + prover_mode: config.prover_mode, + google_jwks_url: config.google_jwks_url.clone(), }) } @@ -118,10 +155,22 @@ impl ProofClient { image_signature: &[u8], ) -> Result { // Check for mock mode - if self.service_url.starts_with("mock://") { + if self.prover_mode == ProverMode::Mock { return self.generate_mock_proof(evm_public_key); } + // Stylus mode: construct proof locally (no external service) + if self.prover_mode == ProverMode::Stylus { + return self + .generate_stylus_proof( + jwt_token, + expected_audience, + evm_public_key, + image_signature, + ) + .await; + } + let request = ProofRequest { jwt_token: jwt_token.to_string(), expected_audience: expected_audience.to_string(), @@ -196,7 +245,7 @@ impl ProofClient { /// Check if the proof service is healthy pub async fn health_check(&self) -> Result { - if self.service_url.starts_with("mock://") { + if self.prover_mode == ProverMode::Mock || self.prover_mode == ProverMode::Stylus { return Ok(true); } @@ -215,6 +264,156 @@ impl ProofClient { Ok(response.status().is_success()) } + /// Generate proof data for Stylus on-chain JWT verification. + /// + /// No external service is needed. This method: + /// 1. Parses the JWT locally to extract claims + /// 2. Fetches the JWK RSA public key from Google's JWKS endpoint + /// 3. Constructs the `PublicValuesStruct` and `StylusProofData` + /// 4. The Stylus contract will verify the JWT RS256 signature on-chain + async fn generate_stylus_proof( + &self, + jwt_token: &str, + expected_audience: &str, + evm_public_key: &[u8; 64], + image_signature: &[u8], + ) -> Result { + info!("Constructing Stylus proof data locally (no external service)"); + + // Parse the JWT to extract header and claims + let parts: Vec<&str> = jwt_token.splitn(3, '.').collect(); + if parts.len() != 3 { + return Err(BootstrapError::ProofGenerationFailed( + "Invalid JWT format: expected 3 dot-separated parts".into(), + )); + } + + let header_bytes = base64url_decode(parts[0]).map_err(|e| { + BootstrapError::ProofGenerationFailed(format!("Failed to decode JWT header: {e}")) + })?; + let payload_bytes = base64url_decode(parts[1]).map_err(|e| { + BootstrapError::ProofGenerationFailed(format!("Failed to decode JWT payload: {e}")) + })?; + + let header: JwtHeader = serde_json::from_slice(&header_bytes).map_err(|e| { + BootstrapError::ProofGenerationFailed(format!("Failed to parse JWT header: {e}")) + })?; + let claims: JwtClaims = serde_json::from_slice(&payload_bytes).map_err(|e| { + BootstrapError::ProofGenerationFailed(format!("Failed to parse JWT claims: {e}")) + })?; + + // Validate audience matches expected value + if claims.aud != expected_audience { + return Err(BootstrapError::ProofGenerationFailed(format!( + "JWT audience mismatch: expected '{}', got '{}'", + expected_audience, claims.aud + ))); + } + + // Derive TEE address from public key + let pubkey_hash = keccak256(evm_public_key); + let tee_address = Address::from_slice(&pubkey_hash[12..]); + + // Parse image signature (r || s || v, 65 bytes) + if image_signature.len() != 65 { + return Err(BootstrapError::ProofGenerationFailed(format!( + "Image signature must be 65 bytes, got {}", + image_signature.len() + ))); + } + let sig_r: [u8; 32] = image_signature[0..32].try_into().unwrap(); + let sig_s: [u8; 32] = image_signature[32..64].try_into().unwrap(); + let sig_v: u8 = image_signature[64]; + + // Extract image digest from claims + let image_digest = claims.image_digest().ok_or_else(|| { + BootstrapError::ProofGenerationFailed( + "JWT claims missing submods.container.image_digest".into(), + ) + })?; + + // Construct PublicValuesStruct + let public_values = PublicValuesStruct { + jwk_key_hash: keccak256(header.kid.as_bytes()), + validity_window_start: claims.iat, + validity_window_end: claims.exp, + image_digest_hash: keccak256(image_digest.as_bytes()), + tee_signing_key: tee_address, + secboot: claims.secboot.unwrap_or(false), + dbgstat_disabled: claims.dbgstat.as_deref() == Some("disabled-since-boot"), + audience_hash: keccak256(claims.aud.as_bytes()), + image_signature_v: sig_v, + image_signature_r: FixedBytes::from(sig_r), + image_signature_s: FixedBytes::from(sig_s), + }; + let public_values_encoded = public_values.abi_encode(); + + // Fetch the JWK RSA public key from Google's JWKS endpoint + let jwk = self.fetch_jwk_by_kid(&header.kid).await?; + let modulus_bytes = base64url_decode(&jwk.n).map_err(|e| { + BootstrapError::ProofGenerationFailed(format!("Failed to decode JWK modulus: {e}")) + })?; + let exponent_bytes = base64url_decode(&jwk.e).map_err(|e| { + BootstrapError::ProofGenerationFailed(format!("Failed to decode JWK exponent: {e}")) + })?; + + info!( + kid = %header.kid, + modulus_len = modulus_bytes.len(), + "Fetched JWK RSA public key from Google" + ); + + // Construct StylusProofData (raw JWT + JWK key material for on-chain verification) + let proof_data = StylusProofData { + jwt: jwt_token.as_bytes().to_vec().into(), + jwk_modulus: modulus_bytes.into(), + jwk_exponent: exponent_bytes.into(), + }; + let proof_bytes_encoded = proof_data.abi_encode(); + + info!( + tee_address = %tee_address, + "Stylus proof data constructed locally" + ); + + Ok(ProofResponse { + public_values: format!("0x{}", hex::encode(&public_values_encoded)), + proof_bytes: format!("0x{}", hex::encode(&proof_bytes_encoded)), + tee_address: format!("{tee_address}"), + }) + } + + /// Fetch JWK (JSON Web Key) by key ID from Google's JWKS endpoint. + async fn fetch_jwk_by_kid(&self, kid: &str) -> Result { + let response = self + .client + .get(&self.google_jwks_url) + .timeout(JWKS_FETCH_TIMEOUT) + .send() + .await + .map_err(|e| { + BootstrapError::ProofGenerationFailed(format!("Failed to fetch JWKS: {e}")) + })?; + + if !response.status().is_success() { + return Err(BootstrapError::ProofGenerationFailed(format!( + "JWKS fetch failed: HTTP {}", + response.status() + ))); + } + + let jwks: JwksResponse = response.json().await.map_err(|e| { + BootstrapError::ProofGenerationFailed(format!("Failed to parse JWKS response: {e}")) + })?; + + jwks.keys.into_iter().find(|k| k.kid == kid).ok_or_else(|| { + BootstrapError::ProofGenerationFailed(format!( + "JWK with kid '{}' not found in JWKS response", + kid + )) + }) + } + /// Generate a mock proof for testing fn generate_mock_proof( &self, @@ -223,26 +422,186 @@ impl ProofClient { warn!("Using MOCK prover - proofs will NOT be valid on-chain"); // Derive address from public key (same as EvmKeyManager) - let hash = alloy::primitives::keccak256(evm_public_key); + let hash = keccak256(evm_public_key); let address = Address::from_slice(&hash[12..]); debug!(address = %address, "Generated mock proof"); - // Build ABI-encoded public values with correct tee_address placement - // PublicValuesStruct has 11 fields × 32 bytes = 352 bytes ABI-encoded - // Slot 4 (bytes 128-160): tee_signing_key (address is right-aligned, bytes 140-160) - let mut public_values_bytes = vec![0u8; 352]; - // Place address at bytes 140-160 (right-aligned in 32-byte slot 4) - public_values_bytes[140..160].copy_from_slice(address.as_slice()); + let public_values = PublicValuesStruct { + jwk_key_hash: FixedBytes::ZERO, + validity_window_start: 0, + validity_window_end: 0, + image_digest_hash: FixedBytes::ZERO, + tee_signing_key: address, + secboot: false, + dbgstat_disabled: false, + audience_hash: FixedBytes::ZERO, + image_signature_v: 0, + image_signature_r: FixedBytes::ZERO, + image_signature_s: FixedBytes::ZERO, + }; + let public_values_encoded = public_values.abi_encode(); Ok(ProofResponse { - public_values: format!("0x{}", hex::encode(&public_values_bytes)), + public_values: format!("0x{}", hex::encode(&public_values_encoded)), proof_bytes: "0x".into(), tee_address: format!("{address}"), }) } } +// -- JWT / JWKS types for local parsing -- + +/// Minimal JWT header for extracting kid and alg +#[derive(Debug, Deserialize)] +struct JwtHeader { + kid: String, + #[allow(dead_code)] + alg: String, +} + +/// Minimal JWT claims for constructing `PublicValuesStruct` +#[derive(Debug, Deserialize)] +struct JwtClaims { + aud: String, + iat: u64, + exp: u64, + #[serde(default)] + secboot: Option, + #[serde(default)] + dbgstat: Option, + #[serde(default)] + submods: Option, +} + +impl JwtClaims { + fn image_digest(&self) -> Option<&str> { + self.submods + .as_ref() + .and_then(|s| s.container.as_ref()) + .and_then(|c| c.image_digest.as_deref()) + } +} + +#[derive(Debug, Deserialize)] +struct JwtSubmods { + container: Option, +} + +#[derive(Debug, Deserialize)] +struct JwtContainer { + image_digest: Option, +} + +/// Google JWKS response +#[derive(Debug, Deserialize)] +struct JwksResponse { + keys: Vec, +} + +/// Individual JWK key from Google's JWKS endpoint +#[derive(Debug, Deserialize)] +struct JwkKeyResponse { + kid: String, + /// RSA modulus (base64url-encoded) + n: String, + /// RSA exponent (base64url-encoded) + e: String, +} + +/// Decode base64url to bytes, handling missing padding. +fn base64url_decode(input: &str) -> Result, String> { + // Convert base64url to standard base64 + let mut standard = String::with_capacity(input.len() + 4); + for c in input.chars() { + match c { + '-' => standard.push('+'), + '_' => standard.push('/'), + c => standard.push(c), + } + } + + // Add padding + match standard.len() % 4 { + 2 => standard.push_str("=="), + 3 => standard.push('='), + _ => {} + } + + base64_decode(&standard) +} + +fn base64_decode(input: &str) -> Result, String> { + const DECODE_TABLE: [i8; 128] = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, -1, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, + -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, + 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + ]; + + let bytes = input.as_bytes(); + if !bytes.len().is_multiple_of(4) { + return Err("Invalid base64 length".into()); + } + + let mut output = Vec::with_capacity(bytes.len() * 3 / 4); + let mut i = 0; + + while i < bytes.len() { + let a = bytes[i]; + let b = bytes[i + 1]; + let c = bytes[i + 2]; + let d = bytes[i + 3]; + + let va = if a < 128 { + DECODE_TABLE[a as usize] + } else { + -1 + }; + let vb = if b < 128 { + DECODE_TABLE[b as usize] + } else { + -1 + }; + + if va < 0 || vb < 0 { + return Err("Invalid base64 character".into()); + } + + output.push(((va as u8) << 2) | ((vb as u8) >> 4)); + + if c != b'=' { + let vc = if c < 128 { + DECODE_TABLE[c as usize] + } else { + -1 + }; + if vc < 0 { + return Err("Invalid base64 character".into()); + } + output.push(((vb as u8) << 4) | ((vc as u8) >> 2)); + + if d != b'=' { + let vd = if d < 128 { + DECODE_TABLE[d as usize] + } else { + -1 + }; + if vd < 0 { + return Err("Invalid base64 character".into()); + } + output.push(((vc as u8) << 6) | (vd as u8)); + } + } + + i += 4; + } + + Ok(output) +} + #[cfg(test)] mod tests { use super::*; @@ -255,6 +614,89 @@ mod tests { }; let client = ProofClient::from_config(&config).unwrap(); - assert!(client.service_url.starts_with("mock://")); + assert_eq!(client.prover_mode, ProverMode::Mock); + } + + #[test] + fn test_stylus_prover_no_service_url() { + let config = BootstrapConfig { + prover_mode: ProverMode::Stylus, + ..Default::default() + }; + + let client = ProofClient::from_config(&config).unwrap(); + assert!(client.service_url.is_empty()); + assert_eq!(client.prover_mode, ProverMode::Stylus); + } + + #[test] + fn test_base64url_decode() { + assert_eq!(base64url_decode("SGVsbG8").unwrap(), b"Hello"); + assert_eq!(base64url_decode("PDw_Pz4-").unwrap(), b"<>".to_vec()); + } + + #[test] + fn test_jwt_claims_parsing() { + let claims_json = br#"{ + "aud": "https://test.example.com", + "iat": 1764707757, + "exp": 1764711357, + "secboot": true, + "dbgstat": "disabled-since-boot", + "submods": { + "container": { + "image_digest": "sha256:abc123" + } + } + }"#; + + let claims: JwtClaims = serde_json::from_slice(claims_json).unwrap(); + assert_eq!(claims.aud, "https://test.example.com"); + assert_eq!(claims.iat, 1764707757); + assert_eq!(claims.exp, 1764711357); + assert_eq!(claims.secboot, Some(true)); + assert_eq!(claims.dbgstat.as_deref(), Some("disabled-since-boot")); + assert_eq!(claims.image_digest(), Some("sha256:abc123")); + } + + #[test] + fn test_public_values_abi_encoding() { + let values = PublicValuesStruct { + jwk_key_hash: FixedBytes::ZERO, + validity_window_start: 1000, + validity_window_end: 2000, + image_digest_hash: FixedBytes::ZERO, + tee_signing_key: Address::ZERO, + secboot: true, + dbgstat_disabled: true, + audience_hash: FixedBytes::ZERO, + image_signature_v: 27, + image_signature_r: FixedBytes::ZERO, + image_signature_s: FixedBytes::ZERO, + }; + + let encoded = values.abi_encode(); + // 11 fields x 32 bytes each = 352 bytes + assert_eq!(encoded.len(), 352); + + let decoded = PublicValuesStruct::abi_decode(&encoded).unwrap(); + assert_eq!(decoded.validity_window_start, 1000); + assert_eq!(decoded.validity_window_end, 2000); + assert!(decoded.secboot); + } + + #[test] + fn test_stylus_proof_data_abi_encoding() { + let proof = StylusProofData { + jwt: b"jwt-bytes".to_vec().into(), + jwk_modulus: b"modulus".to_vec().into(), + jwk_exponent: b"exponent".to_vec().into(), + }; + + let encoded = proof.abi_encode(); + let decoded = StylusProofData::abi_decode(&encoded).unwrap(); + assert_eq!(decoded.jwt.as_ref(), b"jwt-bytes"); + assert_eq!(decoded.jwk_modulus.as_ref(), b"modulus"); + assert_eq!(decoded.jwk_exponent.as_ref(), b"exponent"); } } diff --git a/crates/synddb-bootstrap/src/state_machine.rs b/crates/synddb-bootstrap/src/state_machine.rs index a9baf3fa..8eab86ce 100644 --- a/crates/synddb-bootstrap/src/state_machine.rs +++ b/crates/synddb-bootstrap/src/state_machine.rs @@ -26,7 +26,7 @@ pub enum BootstrapState { GeneratingKey, /// Fetching attestation token FetchingAttestation, - /// Generating RISC Zero proof + /// Generating attestation proof GeneratingProof, /// Registering key via relayer RegisteringKey, diff --git a/scripts/deploy-stylus.sh b/scripts/deploy-stylus.sh new file mode 100755 index 00000000..c4040729 --- /dev/null +++ b/scripts/deploy-stylus.sh @@ -0,0 +1,478 @@ +#!/bin/bash +# +# Deploy the SyndDB Stylus Attestation Verifier to Arbitrum +# +# This script handles the full lifecycle: +# 1. Check - Verify the contract compiles and fits within Stylus limits +# 2. Deploy - Submit the WASM bytecode and activate on-chain +# 3. Init - Call initialize() and configure trusted JWKs, image digests, signers +# 4. Verify - Optionally verify the deployment via reproducible build +# +# Usage: +# # Deploy to Arbitrum Sepolia (testnet) +# ./scripts/deploy-stylus.sh \ +# --endpoint https://sepolia-rollup.arbitrum.io/rpc \ +# --private-key-path ./deployer-key.txt \ +# --expiration-tolerance 300 +# +# # Deploy to local Nitro devnode +# ./scripts/deploy-stylus.sh \ +# --endpoint http://localhost:8547 \ +# --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659 +# +# # Check only (no deployment) +# ./scripts/deploy-stylus.sh --check-only --endpoint https://sepolia-rollup.arbitrum.io/rpc +# +# # Export ABI only +# ./scripts/deploy-stylus.sh --export-abi +# +# # Initialize an already-deployed contract +# ./scripts/deploy-stylus.sh \ +# --init-only \ +# --contract-address 0x1234...abcd \ +# --endpoint https://sepolia-rollup.arbitrum.io/rpc \ +# --private-key-path ./deployer-key.txt \ +# --expiration-tolerance 300 +# +# Environment Variables (alternative to CLI flags): +# STYLUS_RPC_URL - RPC endpoint URL +# STYLUS_PRIVATE_KEY - Deployer private key (hex string) +# STYLUS_PRIVATE_KEY_PATH - Path to file containing private key +# EXPIRATION_TOLERANCE - JWT expiration tolerance in seconds (default: 300) +# +# Post-deployment configuration (run separately via cast): +# cast send "addTrustedJwk(bytes32,bytes32)" +# cast send "addAllowedImageDigestHash(bytes32)" +# cast send "addTrustedImageSigner(address)" + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +CONTRACT_DIR="$PROJECT_ROOT/contracts/stylus/attestation-verifier" +OUTPUT_DIR="$PROJECT_ROOT/.synddb" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { echo -e "${BLUE}[stylus]${NC} $1"; } +success() { echo -e "${GREEN}[stylus]${NC} $1"; } +warn() { echo -e "${YELLOW}[stylus]${NC} $1"; } +error() { echo -e "${RED}[stylus]${NC} $1" >&2; } + +# Default values +ENDPOINT="${STYLUS_RPC_URL:-}" +PRIVATE_KEY="${STYLUS_PRIVATE_KEY:-}" +PRIVATE_KEY_PATH="${STYLUS_PRIVATE_KEY_PATH:-}" +EXPIRATION_TOLERANCE="${EXPIRATION_TOLERANCE:-300}" +CONTRACT_ADDRESS="" +CHECK_ONLY=false +EXPORT_ABI=false +INIT_ONLY=false +NO_ACTIVATE=false +NO_VERIFY=false +ESTIMATE_GAS=false +SKIP_INIT=false + +usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Deploy the SyndDB Stylus Attestation Verifier to Arbitrum" + echo "" + echo "Options:" + echo " --endpoint URL RPC endpoint (or set STYLUS_RPC_URL)" + echo " --private-key KEY Deployer private key (hex)" + echo " --private-key-path PATH Path to file containing private key" + echo " --expiration-tolerance N JWT expiration tolerance in seconds (default: 300)" + echo " --contract-address ADDR Contract address (for --init-only)" + echo " --check-only Only run cargo stylus check, don't deploy" + echo " --export-abi Export the contract ABI and exit" + echo " --init-only Skip deployment, only initialize an existing contract" + echo " --skip-init Deploy but don't call initialize()" + echo " --no-activate Deploy without activating (activate separately later)" + echo " --no-verify Skip Docker-based reproducible build verification" + echo " --estimate-gas Estimate deployment gas cost without deploying" + echo " --help Show this help" + exit 0 +} + +# Helper for flags that require a value +require_arg() { + if [[ $# -lt 2 || "$2" == --* ]]; then + error "$1 requires a value" + exit 1 + fi +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --endpoint) + require_arg "$@" + ENDPOINT="$2" + shift 2 + ;; + --private-key) + require_arg "$@" + PRIVATE_KEY="$2" + shift 2 + ;; + --private-key-path) + require_arg "$@" + PRIVATE_KEY_PATH="$2" + shift 2 + ;; + --expiration-tolerance) + require_arg "$@" + EXPIRATION_TOLERANCE="$2" + shift 2 + ;; + --contract-address) + require_arg "$@" + CONTRACT_ADDRESS="$2" + shift 2 + ;; + --check-only) + CHECK_ONLY=true + shift + ;; + --export-abi) + EXPORT_ABI=true + shift + ;; + --init-only) + INIT_ONLY=true + shift + ;; + --skip-init) + SKIP_INIT=true + shift + ;; + --no-activate) + NO_ACTIVATE=true + shift + ;; + --no-verify) + NO_VERIFY=true + shift + ;; + --estimate-gas) + ESTIMATE_GAS=true + shift + ;; + --help|-h) + usage + ;; + *) + error "Unknown option: $1" + exit 1 + ;; + esac +done + +# --- Validation helpers --- + +require_endpoint() { + if [[ -z "$ENDPOINT" ]]; then + error "No RPC endpoint provided. Use --endpoint or set STYLUS_RPC_URL" + exit 1 + fi +} + +require_private_key() { + if [[ -z "$PRIVATE_KEY" && -z "$PRIVATE_KEY_PATH" ]]; then + error "No private key provided. Use --private-key or --private-key-path" + exit 1 + fi + if [[ -n "$PRIVATE_KEY_PATH" && ! -f "$PRIVATE_KEY_PATH" ]]; then + error "Private key file not found: $PRIVATE_KEY_PATH" + exit 1 + fi +} + +require_tool() { + local tool="$1" + local install_hint="$2" + if ! command -v "$tool" >/dev/null 2>&1; then + error "Required tool not found: $tool" + echo " Install: $install_hint" >&2 + exit 1 + fi +} + +require_cargo_stylus() { + require_tool "cargo" "https://rustup.rs" + if ! cargo stylus --version >/dev/null 2>&1; then + error "Required tool not found: cargo-stylus" + echo " Install: cargo install --force cargo-stylus" >&2 + exit 1 + fi +} + +# Build the key flag for cargo stylus deploy +build_key_arg() { + if [[ -n "$PRIVATE_KEY" ]]; then + echo "--private-key=$PRIVATE_KEY" + elif [[ -n "$PRIVATE_KEY_PATH" ]]; then + echo "--private-key-path=$PRIVATE_KEY_PATH" + fi +} + +# Get the raw private key value for cast commands +get_private_key() { + if [[ -n "$PRIVATE_KEY" ]]; then + echo "$PRIVATE_KEY" + elif [[ -n "$PRIVATE_KEY_PATH" ]]; then + tr -d '[:space:]' < "$PRIVATE_KEY_PATH" + fi +} + +# Extract a 20-byte Ethereum address from text. +# Filters for lines containing "address" to avoid matching tx hashes. +extract_address() { + grep -i 'address' | grep -oi '0x[0-9a-fA-F]\{40\}' | head -1 +} + +# Write deployment info as valid JSON +write_deployment_json() { + local address="$1" + local chain_id="$2" + local deploy_tx="${3:-}" + + # Ensure chain_id is a number, fallback to null for JSON validity + if ! [[ "$chain_id" =~ ^[0-9]+$ ]]; then + chain_id="null" + fi + + # deploymentTx: use JSON null (unquoted) when empty, quoted string otherwise + local deploy_tx_json="null" + if [[ -n "$deploy_tx" ]]; then + deploy_tx_json="\"$deploy_tx\"" + fi + + mkdir -p "$OUTPUT_DIR" + cat > "$OUTPUT_DIR/stylus-deployment.json" <&1) || { + error "Deployment failed:" + echo "$DEPLOY_OUTPUT" + exit 1 +} + +echo "$DEPLOY_OUTPUT" + +# Extract contract address from lines containing "address" to avoid matching tx hashes. +# cargo stylus deploy outputs: "deployed code at address: 0x..." or "contract address: 0x..." +DEPLOYED_ADDRESS=$(echo "$DEPLOY_OUTPUT" | extract_address) + +if [[ -z "$DEPLOYED_ADDRESS" ]]; then + error "Could not parse deployed contract address from output" + error "Check the output above for the address" + exit 1 +fi + +# Extract deployment tx hash if available +DEPLOY_TX=$(echo "$DEPLOY_OUTPUT" | grep -oi 'deployment tx hash: 0x[0-9a-fA-F]\{64\}' | grep -oi '0x[0-9a-fA-F]\{64\}' || echo "") + +success "Contract deployed at: $DEPLOYED_ADDRESS" +echo "" + +# Step 3: Initialize +if [[ "$SKIP_INIT" == "true" ]]; then + warn "Skipping initialization (--skip-init)" +else + log "Step 3/3: Initializing contract..." + log " Expiration tolerance: ${EXPIRATION_TOLERANCE}s" + + CAST_KEY=$(get_private_key) + + cast send \ + --rpc-url "$ENDPOINT" \ + --private-key "$CAST_KEY" \ + "$DEPLOYED_ADDRESS" \ + "initialize(uint64)" \ + "$EXPIRATION_TOLERANCE" + + success "Contract initialized" +fi + +echo "" + +# Save deployment info +CHAIN_ID=$(cast chain-id --rpc-url "$ENDPOINT" 2>/dev/null || echo "unknown") +write_deployment_json "$DEPLOYED_ADDRESS" "$CHAIN_ID" "$DEPLOY_TX" + +success "Deployment info saved to $OUTPUT_DIR/stylus-deployment.json" +echo "" + +# Print summary +echo "================================================================" +success "Deployment complete!" +echo "================================================================" +echo "" +echo " Contract: StylusAttestationVerifier" +echo " Address: $DEPLOYED_ADDRESS" +echo " Chain ID: $CHAIN_ID" +echo " Expiration tolerance: ${EXPIRATION_TOLERANCE}s" +if [[ -n "$DEPLOY_TX" ]]; then + echo " Deployment TX: $DEPLOY_TX" +fi +echo "" +echo " Saved to: $OUTPUT_DIR/stylus-deployment.json" +echo "" + +if [[ -n "$DEPLOY_TX" ]]; then + echo "To verify the deployment (requires Docker):" + echo " cd contracts/stylus/attestation-verifier" + echo " cargo stylus verify --deployment-tx=$DEPLOY_TX --endpoint=$ENDPOINT" + echo "" +fi + +echo "Next steps - configure the verifier:" +echo "" +echo " 1. Add trusted JWK(s) from Google's JWKS endpoint:" +echo " cast send --rpc-url $ENDPOINT --private-key \$KEY \\" +echo " $DEPLOYED_ADDRESS 'addTrustedJwk(bytes32,bytes32)' \$KID_HASH \$KEY_MATERIAL_HASH" +echo "" +echo " 2. Add allowed container image digest(s):" +echo " cast send --rpc-url $ENDPOINT --private-key \$KEY \\" +echo " $DEPLOYED_ADDRESS 'addAllowedImageDigestHash(bytes32)' \$DIGEST_HASH" +echo "" +echo " 3. Add trusted image signer(s):" +echo " cast send --rpc-url $ENDPOINT --private-key \$KEY \\" +echo " $DEPLOYED_ADDRESS 'addTrustedImageSigner(address)' \$SIGNER_ADDRESS" +echo "" +echo " 4. Update TeeKeyManager to use this verifier:" +echo " cast send --rpc-url \$BASE_RPC --private-key \$KEY \\" +echo " \$TEE_KEY_MANAGER 'updateAttestationVerifier(address)' $DEPLOYED_ADDRESS" +echo ""