diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0ff314d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4522 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloy-consensus" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c3f3bc4f2a6b725970cd354e78e9738ea1e8961a91898f57bf6317970b1915" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-trie", + "auto_impl", + "c-kzg", + "derive_more", + "either", + "k256", + "once_cell", + "rand 0.8.5", + "secp256k1", + "serde", + "serde_with", + "thiserror", +] + +[[package]] +name = "alloy-consensus-any" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda014fb5591b8d8d24cab30f52690117d238e52254c6fb40658e91ea2ccd6c3" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-eip2124" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "serde", + "thiserror", +] + +[[package]] +name = "alloy-eip2930" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "serde", + "thiserror", +] + +[[package]] +name = "alloy-eips" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7b2f7010581f29bcace81776cf2f0e022008d05a7d326884763f16f3044620" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "auto_impl", + "c-kzg", + "derive_more", + "either", + "serde", + "sha2", +] + +[[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-json-rpc" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca1e31b50f4ed9a83689ae97263d366b15b935a67c4acb5dd46d5b1c3b27e8e6" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "alloy-network" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879afc0f4a528908c8fe6935b2ab0bc07f77221a989186f71583f7592831689e" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-json-rpc", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-types-any", + "alloy-rpc-types-eth", + "alloy-serde", + "alloy-signer", + "alloy-sol-types", + "async-trait", + "auto_impl", + "derive_more", + "futures-utils-wasm", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "alloy-network-primitives" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec185bac9d32df79c1132558a450d48f6db0bfb5adef417dbb1a0258153f879b" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "serde", +] + +[[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", + "const-hex", + "derive_more", + "foldhash", + "hashbrown 0.15.5", + "indexmap 2.11.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.2", + "ruint", + "rustc-hash", + "serde", + "sha3", + "tiny-keccak", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" +dependencies = [ + "alloy-rlp-derive", + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "alloy-rpc-types-any" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a8f1efd77116915dad61092f9ef9295accd0b0b251062390d9c4e81599344" +dependencies = [ + "alloy-consensus-any", + "alloy-rpc-types-eth", + "alloy-serde", +] + +[[package]] +name = "alloy-rpc-types-eth" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc1323310d87f9d950fb3ff58d943fdf832f5e10e6f902f405c0eaa954ffbaf1" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-sol-types", + "itertools 0.14.0", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "alloy-serde" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05ace2ef3da874544c3ffacfd73261cdb1405d8631765deb991436a53ec6069" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-signer" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fdabad99ad3c71384867374c60bcd311fc1bb90ea87f5f9c779fd8c7ec36aa" +dependencies = [ + "alloy-primitives", + "async-trait", + "auto_impl", + "either", + "elliptic-curve", + "k256", + "thiserror", +] + +[[package]] +name = "alloy-signer-local" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb3f4e72378566b189624d54618c8adf07afbcf39d5f368f4486e35a66725b3" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "k256", + "rand 0.8.5", + "thiserror", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20d867dcf42019d4779519a1ceb55eba8d7f3d0e4f0a89bcba82b8f9eb01e48" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74e91b0b553c115d14bd0ed41898309356dc85d0e3d4b9014c4e7715e48c8ad" +dependencies = [ + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap 2.11.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.106", + "syn-solidity", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84194d31220803f5f62d0a00f583fd3a062b36382e2bea446f1af96727754565" +dependencies = [ + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "syn 2.0.106", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe8c27b3cf6b2bb8361904732f955bc7c05e00be5f469cec7e2280b6167f3ff0" +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 = "alloy-trie" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "983d99aa81f586cef9dae38443245e585840fcf0fc58b09aee0b1f27aed1d500" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arrayvec", + "derive_more", + "nybbles", + "serde", + "smallvec", + "tracing", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff-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" +dependencies = [ + "serde", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin-io" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blst" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd49896f12ac9b6dcd7a5998466b9b58263a695a3dd1ecc1aaca2e12a90b080" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "c-kzg" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7318cfa722931cb5fe0838b98d3ce5621e75f6a6408abc21721d80de9223f2e4" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[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.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dccd746bf9b1038c0507b7cec21eb2b11222db96a2902c96e8c185d6d20fb9c4" +dependencies = [ + "cfg-if", + "cpufeatures", + "hex", + "proptest", + "serde", +] + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +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 = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "deadpool" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[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_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "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 = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.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 = "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 = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "libz-rs-sys", + "miniz_oxide", +] + +[[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 = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "futures-utils-wasm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.3+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[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 = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.11.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", + "serde", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown 0.15.5", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "serdect", + "sha2", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keccak-asm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "liblzma" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10bf66f4598dc77ff96677c8e763655494f00ff9c1cf79e2eb5bb07bc31f807d" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +dependencies = [ + "value-bag", +] + +[[package]] +name = "logtest" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3e43a8657c1d64516dcc9db8ca03826a4aceaf89d5ce1b37b59f6ff0e43026" +dependencies = [ + "lazy_static", + "log", +] + +[[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.106", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[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-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[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-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "nybbles" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307" +dependencies = [ + "alloy-rlp", + "const-hex", + "proptest", + "serde", + "smallvec", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[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.106", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppmd-rust" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[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.106", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "lazy_static", + "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.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +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", + "serde", +] + +[[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.3", + "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.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", + "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.3", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "ruint" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecb38f82477f20c5c3d62ef52d7c4e536e38ea9b73fb570a20c5cae0e14bcf6" +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-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[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.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.26", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[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.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.11.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha256" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2", + "tokio", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" +dependencies = [ + "cc", + "cfg-if", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[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 = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[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 = "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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-solidity" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b198d366dbec045acfcd97295eb653a7a2b40e4dc764ef1e79aafcad439d3c" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tee-worker-post-compute" +version = "0.1.0" +dependencies = [ + "aes", + "alloy-signer", + "alloy-signer-local", + "base64", + "cbc", + "env_logger", + "log", + "logtest", + "mockall", + "once_cell", + "rand 0.8.5", + "reqwest", + "rsa", + "serde", + "serde_json", + "serial_test", + "sha256", + "sha3", + "temp-env", + "tempfile", + "thiserror", + "tokio", + "walkdir", + "wiremock", + "zip", +] + +[[package]] +name = "temp-env" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.11.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[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 = "value-bag" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[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 = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.3+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + +[[package]] +name = "wit-bindgen" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zip" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835eb39822904d39cb19465de1159e05d371973f0c6df3a365ad50565ddc8b9" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.3", + "hmac", + "indexmap 2.11.0", + "liblzma", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index ae06cf1..e8c396e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,5 @@ [workspace] resolver = "3" -members = ["add"] +members = [ + "post-compute" +] diff --git a/add/Cargo.toml b/add/Cargo.toml deleted file mode 100644 index 9cc0d91..0000000 --- a/add/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "add" -version = "0.1.0" -edition = "2024" - -[dependencies] diff --git a/add/src/lib.rs b/add/src/lib.rs deleted file mode 100644 index b93cf3f..0000000 --- a/add/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} diff --git a/post-compute/Cargo.toml b/post-compute/Cargo.toml new file mode 100644 index 0000000..8e11fd8 --- /dev/null +++ b/post-compute/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "tee-worker-post-compute" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "tee-worker-post-compute" +path = "src/bin/tee_worker_post_compute.rs" + +[dependencies] +aes = "0.8.4" +alloy-signer = "0.15.9" +alloy-signer-local = "0.15.9" +cbc = { version = "0.1.2", features = ["alloc"] } +env_logger = "0.11.8" +base64 = "0.22.1" +log = "0.4.27" +rand = "0.8.5" +rsa = "0.9.8" +reqwest = { version = "0.12.15", features = ["blocking", "json"] } +serde = "1.0.219" +serde_json = "1.0.140" +sha256 = "1.6.0" +sha3 = "0.10.8" +thiserror = "2.0.12" +walkdir = "2.5.0" +zip = "4.0.0" + +[dev-dependencies] +logtest = "2.0.0" +mockall = "0.13.1" +once_cell = "1.21.3" +serial_test = "3.2.0" +temp-env = "0.3.6" +tempfile = "3.20.0" +tokio = "1.45.0" +wiremock = "0.6.3" diff --git a/post-compute/src/api.rs b/post-compute/src/api.rs new file mode 100644 index 0000000..edfc935 --- /dev/null +++ b/post-compute/src/api.rs @@ -0,0 +1,2 @@ +pub mod result_proxy_api_client; +pub mod worker_api; diff --git a/post-compute/src/api/result_proxy_api_client.rs b/post-compute/src/api/result_proxy_api_client.rs new file mode 100644 index 0000000..a46904e --- /dev/null +++ b/post-compute/src/api/result_proxy_api_client.rs @@ -0,0 +1,318 @@ +use reqwest::blocking::Client; +use serde::{Deserialize, Serialize}; + +const EMPTY_HEX_STRING_32: &str = + "0x0000000000000000000000000000000000000000000000000000000000000000"; +const EMPTY_WEB3_SIG: &str = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + +/// Represents a computation result that can be uploaded to IPFS via the iExec result proxy. +/// +/// This struct encapsulates all the necessary information about a completed computation task +/// that needs to be stored permanently on IPFS. It includes task identification, metadata, +/// the actual result data, and cryptographic proofs of computation integrity. +/// +/// The struct is designed to be serialized to JSON for transmission to the result proxy API, +/// with field names automatically converted to camelCase to match the expected API format. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResultModel { + /// Unique identifier of the task on the blockchain + pub chain_task_id: String, + /// Unique identifier of the deal this task belongs to + pub deal_id: String, + /// Index of the task within the deal + pub task_index: u32, + /// Compressed result data as a byte array + pub zip: Vec, + /// Cryptographic hash of the computation result + pub determinist_hash: String, + /// TEE (Trusted Execution Environment) signature proving integrity + pub enclave_signature: String, +} + +impl Default for ResultModel { + fn default() -> Self { + Self { + chain_task_id: EMPTY_HEX_STRING_32.to_string(), + deal_id: EMPTY_HEX_STRING_32.to_string(), + task_index: 0, + zip: vec![], + determinist_hash: String::new(), + enclave_signature: EMPTY_WEB3_SIG.to_string(), + } + } +} + +pub struct ResultProxyApiClient { + base_url: String, + client: Client, +} + +impl ResultProxyApiClient { + /// Creates a new HTTP client for interacting with the iExec result proxy API. + /// + /// This function initializes a client with the provided base URL. The client can then be used + /// to upload computation results to IPFS via the result proxy service. + /// + /// # Arguments + /// + /// * `base_url` - The base URL of the result proxy service (e.g., "") + /// + /// # Returns + /// + /// A new `ResultProxyApiClient` instance configured with the provided base URL. + /// + /// # Example + /// + /// ```rust + /// use tee_worker_post_compute::api::result_proxy_api_client::ResultProxyApiClient; + /// + /// let client = ResultProxyApiClient::new("https://result.v8-bellecour.iex.ec"); + /// ``` + pub fn new(base_url: &str) -> Self { + Self { + base_url: base_url.to_string(), + client: Client::new(), + } + } + + /// Uploads a computation result to IPFS via the result proxy service. + /// + /// This method sends a POST request to the result proxy's `/v1/results` endpoint with + /// the provided result model. The result proxy validates the data, uploads it to IPFS, + /// and returns the IPFS link for permanent storage. + /// + /// The upload process involves several steps handled by the result proxy: + /// 1. Authentication and authorization validation + /// 2. Result data validation (signatures, hashes, etc.) + /// 3. IPFS upload and pinning + /// 4. Registration of the result link on the blockchain + /// + /// # Arguments + /// + /// * `authorization` - The bearer token for authenticating with the result proxy + /// * `result_model` - The [`ResultModel`] containing the computation result to upload + /// + /// # Returns + /// + /// * `Ok(String)` - The IPFS link where the result was uploaded (e.g., "ipfs://QmHash...") + /// * `Err(reqwest::Error)` - HTTP client error or server-side error + /// + /// # Errors + /// + /// This function will return an error in the following situations: + /// * Network connectivity issues preventing the HTTP request + /// * Authentication failures (invalid or expired token) + /// * Server-side validation failures (invalid signatures, malformed data) + /// * IPFS upload failures on the result proxy side + /// * HTTP status codes indicating server errors (4xx, 5xx) + /// + /// # Example + /// + /// ```rust + /// use tee_worker_post_compute::api::result_proxy_api_client::{ + /// ResultProxyApiClient, + /// ResultModel, + /// }; + /// + /// let client = ResultProxyApiClient::new("https://result-proxy.iex.ec"); + /// let result_model = ResultModel { + /// chain_task_id: "0x123...".to_string(), + /// zip: vec![0xde, 0xad, 0xbe, 0xef], + /// determinist_hash: "0xabc".to_string(), + /// enclave_signature: "0xdef".to_string(), + /// ..Default::default() + /// }; + /// + /// match client.upload_to_ipfs("Bearer token123", &result_model) { + /// Ok(ipfs_link) => { + /// println!("Successfully uploaded to: {}", ipfs_link); + /// // IPFS link can be used to retrieve the result later + /// } + /// Err(e) => { + /// eprintln!("Upload failed: {}", e); + /// // Handle error appropriately (retry, report, etc.) + /// } + /// } + /// ``` + pub fn upload_to_ipfs( + &self, + authorization: &str, + result_model: &ResultModel, + ) -> Result { + let url = format!("{}/v1/results", self.base_url); + let response = self + .client + .post(&url) + .header("Authorization", authorization) + .json(result_model) + .send()?; + + if response.status().is_success() { + response.text() + } else { + Err(response.error_for_status().unwrap_err()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use wiremock::{ + Mock, MockServer, ResponseTemplate, + matchers::{body_json, header, method, path}, + }; + + // Test constants + const TEST_TASK_ID: &str = "0x123"; + const TEST_DEAL_ID: &str = "0x456"; + const TEST_DETERMINIST_HASH: &str = "0xabc"; + const TEST_ENCLAVE_SIGNATURE: &str = "0xdef"; + const TEST_IPFS_LINK: &str = "ipfs://QmHash123"; + const TEST_TOKEN: &str = "test-token"; + + // region ResultModel + #[test] + fn result_model_default_returns_correct_values_when_created() { + let model = ResultModel::default(); + assert_eq!(model.chain_task_id, EMPTY_HEX_STRING_32); + assert_eq!(model.deal_id, EMPTY_HEX_STRING_32); + assert_eq!(model.task_index, 0); + assert!(model.zip.is_empty()); + assert_eq!(model.determinist_hash, ""); + assert_eq!(model.enclave_signature, EMPTY_WEB3_SIG); + } + + #[test] + fn result_model_serializes_to_camel_case_when_converted_to_json() { + let model = ResultModel { + chain_task_id: TEST_TASK_ID.to_string(), + deal_id: TEST_DEAL_ID.to_string(), + task_index: 5, + zip: vec![1, 2, 3], + determinist_hash: TEST_DETERMINIST_HASH.to_string(), + enclave_signature: TEST_ENCLAVE_SIGNATURE.to_string(), + }; + + let expected = json!({ + "chainTaskId": TEST_TASK_ID, + "dealId": TEST_DEAL_ID, + "taskIndex": 5, + "zip": [1, 2, 3], + "deterministHash": TEST_DETERMINIST_HASH, + "enclaveSignature": TEST_ENCLAVE_SIGNATURE + }); + + let v = serde_json::to_value(model).unwrap(); + assert_eq!(v, expected); + } + + #[test] + fn result_model_deserializes_from_camel_case_when_parsing_json() { + let value = json!({ + "chainTaskId": TEST_TASK_ID, + "dealId": TEST_DEAL_ID, + "taskIndex": 5, + "zip": [1, 2, 3], + "deterministHash": TEST_DETERMINIST_HASH, + "enclaveSignature": TEST_ENCLAVE_SIGNATURE + }); + + let model: ResultModel = serde_json::from_value(value).unwrap(); + + assert_eq!(model.chain_task_id, TEST_TASK_ID); + assert_eq!(model.deal_id, TEST_DEAL_ID); + assert_eq!(model.task_index, 5); + assert_eq!(model.zip, vec![1, 2, 3]); + assert_eq!(model.determinist_hash, TEST_DETERMINIST_HASH); + assert_eq!(model.enclave_signature, TEST_ENCLAVE_SIGNATURE); + } + //endregion + + // region ResultProxyApiClient + #[test] + fn result_proxy_api_client_new_creates_client_when_given_base_url() { + let base_url = "http://localhost:8080"; + let client = ResultProxyApiClient::new(base_url); + assert_eq!(client.base_url, base_url); + } + + #[tokio::test] + async fn upload_to_ipfs_returns_ipfs_link_when_server_responds_successfully() { + let zip_content = b"test content"; + + let expected_model = ResultModel { + chain_task_id: TEST_TASK_ID.to_string(), + determinist_hash: TEST_DETERMINIST_HASH.to_string(), + enclave_signature: TEST_ENCLAVE_SIGNATURE.to_string(), + zip: zip_content.to_vec(), + ..Default::default() + }; + + let mock_server = MockServer::start().await; + let json = serde_json::to_value(&expected_model).unwrap(); + Mock::given(method("POST")) + .and(path("/v1/results")) + .and(header("Authorization", TEST_TOKEN)) + .and(body_json(json)) + .respond_with(ResponseTemplate::new(200).set_body_string(TEST_IPFS_LINK)) + .mount(&mock_server) + .await; + + let result = tokio::task::spawn_blocking(move || { + let client = ResultProxyApiClient::new(&mock_server.uri()); + client.upload_to_ipfs(TEST_TOKEN, &expected_model) + }) + .await + .expect("Task panicked"); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), TEST_IPFS_LINK); + } + + #[tokio::test] + async fn upload_to_ipfs_returns_error_for_all_error_codes() { + let test_cases = vec![ + (400, "400", "Bad Request"), + (401, "401", "Unauthorized"), + (403, "403", "Forbidden"), + (404, "404", "Not Found"), + (500, "500", "Internal Server Error"), + (502, "502", "Bad Gateway"), + (503, "503", "Service Unavailable"), + ]; + + for (status_code, expected_error_contains, description) in test_cases { + let mock_server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/v1/results")) + .respond_with( + ResponseTemplate::new(status_code) + .set_body_string(format!("{status_code} Error")), + ) + .mount(&mock_server) + .await; + + let result = tokio::task::spawn_blocking(move || { + let client = ResultProxyApiClient::new(&mock_server.uri()); + let model = ResultModel::default(); + client.upload_to_ipfs(TEST_TOKEN, &model) + }) + .await + .expect("Task panicked"); + + assert!( + result.is_err(), + "Expected error for status code {status_code} ({description})" + ); + let error = result.unwrap_err(); + assert!( + error.to_string().contains(expected_error_contains), + "Error message should contain '{expected_error_contains}' for status code {status_code} ({description}), but got: {error}" + ); + } + } + // endregion +} diff --git a/post-compute/src/api/worker_api.rs b/post-compute/src/api/worker_api.rs new file mode 100644 index 0000000..7e97552 --- /dev/null +++ b/post-compute/src/api/worker_api.rs @@ -0,0 +1,566 @@ +use crate::compute::{ + computed_file::ComputedFile, + errors::ReplicateStatusCause, + utils::env_utils::{TeeSessionEnvironmentVariable, get_env_var_or_error}, +}; +use log::error; +use reqwest::{blocking::Client, header::AUTHORIZATION}; +use serde::Serialize; + +/// Represents payload that can be sent to the worker API to report the outcome of the +/// post‑compute stage. +/// +/// The JSON structure expected by the REST endpoint is: +/// ```json +/// { +/// "cause": "" +/// } +/// ``` +/// +/// # Arguments +/// +/// * `cause` - A reference to the ReplicateStatusCause indicating why the post-compute operation exited +/// +/// # Example +/// +/// ``` +/// use tee_worker_post_compute::{ +/// api::worker_api::ExitMessage, +/// compute::errors::ReplicateStatusCause, +/// }; +/// +/// let exit_message = ExitMessage::from(&ReplicateStatusCause::PostComputeInvalidTeeSignature); +/// ``` +#[derive(Serialize, Debug)] +pub struct ExitMessage<'a> { + pub cause: &'a ReplicateStatusCause, +} + +impl<'a> From<&'a ReplicateStatusCause> for ExitMessage<'a> { + fn from(cause: &'a ReplicateStatusCause) -> Self { + Self { cause } + } +} + +/// Thin wrapper around a [`Client`] that knows how to reach the iExec worker API. +/// +/// This client can be created directly with a base URL using [`WorkerApiClient::new`], or +/// configured from environment variables using [`WorkerApiClient::from_env`]. +/// +/// # Example +/// +/// ``` +/// use tee_worker_post_compute::api::worker_api::WorkerApiClient; +/// +/// let client = WorkerApiClient::new("http://worker:13100"); +/// ``` +pub struct WorkerApiClient { + base_url: String, + client: Client, +} + +const DEFAULT_WORKER_HOST: &str = "worker:13100"; + +impl WorkerApiClient { + pub fn new(base_url: &str) -> Self { + WorkerApiClient { + base_url: base_url.to_string(), + client: Client::builder().build().unwrap(), + } + } + + /// Creates a new WorkerApiClient instance with configuration from environment variables. + /// + /// This method retrieves the worker host from the [`TeeSessionEnvironmentVariable::WorkerHostEnvVar`] environment variable. + /// If the variable is not set or empty, it defaults to `"worker:13100"`. + /// + /// # Returns + /// + /// * `WorkerApiClient` - A new client configured with the appropriate base URL + /// + /// # Example + /// + /// ``` + /// use tee_worker_post_compute::api::worker_api::WorkerApiClient; + /// + /// let client = WorkerApiClient::from_env(); + /// ``` + pub fn from_env() -> Self { + let worker_host = get_env_var_or_error( + TeeSessionEnvironmentVariable::WorkerHostEnvVar, + ReplicateStatusCause::PostComputeWorkerAddressMissing, + ) + .unwrap_or_else(|_| DEFAULT_WORKER_HOST.to_string()); + + let base_url = format!("http://{worker_host}"); + Self::new(&base_url) + } + + /// Sends an exit cause for a post-compute operation to the Worker API. + /// + /// This method reports the exit cause of a post-compute operation to the Worker API, + /// which can be used for tracking and debugging purposes. + /// + /// # Arguments + /// + /// * `authorization` - The authorization token to use for the API request + /// * `chain_task_id` - The chain task ID for which to report the exit cause + /// * `exit_cause` - The exit cause to report + /// + /// # Returns + /// + /// * `Ok(())` - If the exit cause was successfully reported + /// * `Err(ReplicateStatusCause)` - If the exit cause could not be reported due to an HTTP error + /// + /// # Errors + /// + /// This function will return an [`tee_worker_post_compute::compute::errors::ReplicateStatusCause`] + /// if the request could not be sent or the server responded with a non‑success status. + /// + /// # Example + /// + /// ``` + /// use tee_worker_post_compute::{ + /// api::worker_api::{ExitMessage, WorkerApiClient}, + /// compute::errors::ReplicateStatusCause, + /// }; + /// + /// let client = WorkerApiClient::new("http://worker:13100"); + /// let exit_message = ExitMessage::from(&ReplicateStatusCause::PostComputeInvalidTeeSignature); + /// + /// match client.send_exit_cause_for_post_compute_stage( + /// "authorization_token", + /// "0x123456789abcdef", + /// &exit_message, + /// ) { + /// Ok(()) => println!("Exit cause reported successfully"), + /// Err(error) => eprintln!("Failed to report exit cause: {}", error), + /// } + /// ``` + pub fn send_exit_cause_for_post_compute_stage( + &self, + authorization: &str, + chain_task_id: &str, + exit_cause: &ExitMessage, + ) -> Result<(), ReplicateStatusCause> { + let url = format!("{}/compute/post/{chain_task_id}/exit", self.base_url); + match self + .client + .post(&url) + .header(AUTHORIZATION, authorization) + .json(exit_cause) + .send() + { + Ok(response) => { + if response.status().is_success() { + Ok(()) + } else { + let status = response.status(); + let body = response.text().unwrap_or_default(); + error!( + "Failed to send exit cause to worker: [status:{status:?}, body:{body:#?}]" + ); + Err(ReplicateStatusCause::PostComputeFailedUnknownIssue) + } + } + Err(e) => { + error!("An error occured while sending exit cause to worker: {e}"); + Err(ReplicateStatusCause::PostComputeFailedUnknownIssue) + } + } + } + + /// Sends the completed computed.json file to the worker host. + /// + /// This method transmits the computed file containing task results, signatures, + /// and metadata to the worker API. The computed file is sent as JSON in the + /// request body, allowing the worker to verify and process the computation results. + /// + /// # Arguments + /// + /// * `authorization` - The authorization token/challenge to validate the request on the worker side + /// * `chain_task_id` - The blockchain task identifier associated with this computation + /// * `computed_file` - The computed file containing results and signatures to be sent + /// + /// # Returns + /// + /// * `Ok(())` - If the computed file was successfully sent (HTTP 2xx response) + /// * `Err(Error)` - If the request failed due to an HTTP error + /// + /// # Example + /// + /// ``` + /// use tee_worker_post_compute::{ + /// api::worker_api::WorkerApiClient, + /// compute::computed_file::ComputedFile, + /// }; + /// + /// let client = WorkerApiClient::new("http://worker:13100"); + /// let computed_file = ComputedFile { + /// task_id: Some("0x123456789abcdef".to_string()), + /// result_digest: Some("0xdigest".to_string()), + /// enclave_signature: Some("0xsignature".to_string()), + /// ..Default::default() + /// }; + /// + /// match client.send_computed_file_to_host( + /// "Bearer auth_token", + /// "0x123456789abcdef", + /// &computed_file, + /// ) { + /// Ok(()) => println!("Computed file sent successfully"), + /// Err(error) => eprintln!("Failed to send computed file: {}", error), + /// } + /// ``` + pub fn send_computed_file_to_host( + &self, + authorization: &str, + chain_task_id: &str, + computed_file: &ComputedFile, + ) -> Result<(), ReplicateStatusCause> { + let url = format!("{}/compute/post/{chain_task_id}/computed", self.base_url); + match self + .client + .post(&url) + .header(AUTHORIZATION, authorization) + .json(computed_file) + .send() + { + Ok(response) => { + if response.status().is_success() { + Ok(()) + } else { + let status = response.status(); + let body = response.text().unwrap_or_default(); + error!( + "Failed to send computed file to worker: [status:{status:?}, body:{body:#?}]" + ); + Err(ReplicateStatusCause::PostComputeSendComputedFileFailed) + } + } + Err(e) => { + error!("An error occured while sending computed file to worker: {e}"); + Err(ReplicateStatusCause::PostComputeSendComputedFileFailed) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::compute::utils::env_utils::TeeSessionEnvironmentVariable::*; + use logtest::Logger; + use once_cell::sync::Lazy; + use serde_json::{json, to_string}; + use serial_test::serial; + use std::sync::Mutex; + use temp_env::with_vars; + use wiremock::{ + Mock, MockServer, ResponseTemplate, + matchers::{body_json, header, method, path}, + }; + + static TEST_LOGGER: Lazy> = Lazy::new(|| Mutex::new(Logger::start())); + + const CHALLENGE: &str = "challenge"; + const CHAIN_TASK_ID: &str = "0x123456789abcdef"; + + // region ExitMessage() + #[test] + fn should_serialize_exit_message() { + let causes = [ + ( + ReplicateStatusCause::PostComputeInvalidTeeSignature, + "POST_COMPUTE_INVALID_TEE_SIGNATURE", + ), + ( + ReplicateStatusCause::PostComputeWorkerAddressMissing, + "POST_COMPUTE_WORKER_ADDRESS_MISSING", + ), + ( + ReplicateStatusCause::PostComputeFailedUnknownIssue, + "POST_COMPUTE_FAILED_UNKNOWN_ISSUE", + ), + ]; + + for (cause, message) in causes { + let exit_message = ExitMessage::from(&cause); + let serialized = to_string(&exit_message).expect("Failed to serialize"); + let expected = format!("{{\"cause\":\"{message}\"}}"); + assert_eq!(serialized, expected); + } + } + // endregion + + // region get_worker_api_client + #[test] + fn should_get_worker_api_client_with_env_var() { + with_vars( + vec![(WorkerHostEnvVar.name(), Some("custom-worker-host:9999"))], + || { + let client = WorkerApiClient::from_env(); + assert_eq!(client.base_url, "http://custom-worker-host:9999"); + }, + ); + } + + #[test] + fn should_get_worker_api_client_without_env_var() { + with_vars(vec![(WorkerHostEnvVar.name(), None::<&str>)], || { + let client = WorkerApiClient::from_env(); + assert_eq!(client.base_url, format!("http://{DEFAULT_WORKER_HOST}")); + }); + } + // endregion + + // region send_exit_cause_for_post_compute_stage() + #[tokio::test] + async fn should_send_exit_cause() { + let mock_server = MockServer::start().await; + let server_url = mock_server.uri(); + + let expected_body = json!({ + "cause": ReplicateStatusCause::PostComputeInvalidTeeSignature, + }); + + Mock::given(method("POST")) + .and(path(format!("/compute/post/{CHAIN_TASK_ID}/exit"))) + .and(header("Authorization", CHALLENGE)) + .and(body_json(&expected_body)) + .respond_with(ResponseTemplate::new(200)) + .expect(1) + .mount(&mock_server) + .await; + + let result = tokio::task::spawn_blocking(move || { + let exit_message = + ExitMessage::from(&ReplicateStatusCause::PostComputeInvalidTeeSignature); + let worker_api_client = WorkerApiClient::new(&server_url); + worker_api_client.send_exit_cause_for_post_compute_stage( + CHALLENGE, + CHAIN_TASK_ID, + &exit_message, + ) + }) + .await + .expect("Task panicked"); + + assert!(result.is_ok()); + } + + #[tokio::test] + #[serial] + async fn should_not_send_exit_cause() { + { + let mut logger = TEST_LOGGER.lock().unwrap(); + while logger.pop().is_some() {} + } + let mock_server = MockServer::start().await; + let server_url = mock_server.uri(); + + Mock::given(method("POST")) + .and(path(format!("/compute/post/{CHAIN_TASK_ID}/exit"))) + .respond_with(ResponseTemplate::new(404)) + .expect(1) + .mount(&mock_server) + .await; + + let result = tokio::task::spawn_blocking(move || { + let exit_message = + ExitMessage::from(&ReplicateStatusCause::PostComputeFailedUnknownIssue); + let worker_api_client = WorkerApiClient::new(&server_url); + worker_api_client.send_exit_cause_for_post_compute_stage( + CHALLENGE, + CHAIN_TASK_ID, + &exit_message, + ) + }) + .await + .expect("Task panicked"); + + assert!(result.is_err()); + + if let Err(error) = result { + assert_eq!( + error, + ReplicateStatusCause::PostComputeFailedUnknownIssue, + "Expected PostComputeFailedUnknownIssue, got: {error:?}" + ); + } + let mut logger = TEST_LOGGER.lock().unwrap(); + let mut found = false; + while let Some(rec) = logger.pop() { + if rec.args().contains("status:404") { + found = true; + break; + } + } + assert!(found, "Expected log to contain HTTP 404 status"); + } + // endregion + + // region send_computed_file_to_host() + #[tokio::test] + async fn should_send_computed_file_successfully() { + let mock_server = MockServer::start().await; + let server_uri = mock_server.uri(); + + let computed_file = ComputedFile { + task_id: Some(CHAIN_TASK_ID.to_string()), + result_digest: Some("0xdigest".to_string()), + enclave_signature: Some("0xsignature".to_string()), + ..Default::default() + }; + + let expected_path = format!("/compute/post/{CHAIN_TASK_ID}/computed"); + let expected_body = json!(computed_file); + + Mock::given(method("POST")) + .and(path(expected_path.as_str())) + .and(header("Authorization", CHALLENGE)) + .and(body_json(&expected_body)) + .respond_with(ResponseTemplate::new(200)) + .expect(1) + .mount(&mock_server) + .await; + + let result = tokio::task::spawn_blocking(move || { + let client = WorkerApiClient::new(&server_uri); + client.send_computed_file_to_host(CHALLENGE, CHAIN_TASK_ID, &computed_file) + }) + .await + .expect("Task panicked"); + + assert!(result.is_ok()); + } + + #[tokio::test] + #[serial] + async fn should_fail_send_computed_file_on_server_error() { + { + let mut logger = TEST_LOGGER.lock().unwrap(); + while logger.pop().is_some() {} + } + let mock_server = MockServer::start().await; + let server_uri = mock_server.uri(); + + let computed_file = ComputedFile { + task_id: Some(CHAIN_TASK_ID.to_string()), + result_digest: Some("0xdigest".to_string()), + enclave_signature: Some("0xsignature".to_string()), + ..Default::default() + }; + let expected_path = format!("/compute/post/{CHAIN_TASK_ID}/computed"); + let expected_body = json!(computed_file); + + Mock::given(method("POST")) + .and(path(expected_path.as_str())) + .and(header("Authorization", CHALLENGE)) + .and(body_json(&expected_body)) + .respond_with(ResponseTemplate::new(500)) + .expect(1) + .mount(&mock_server) + .await; + + let result = tokio::task::spawn_blocking(move || { + let client = WorkerApiClient::new(&server_uri); + client.send_computed_file_to_host(CHALLENGE, CHAIN_TASK_ID, &computed_file) + }) + .await + .expect("Task panicked"); + + assert!(result.is_err()); + if let Err(error) = result { + assert_eq!( + error, + ReplicateStatusCause::PostComputeSendComputedFileFailed, + "Expected PostComputeSendComputedFileFailed, got: {error:?}" + ); + } + let mut logger = TEST_LOGGER.lock().unwrap(); + let mut found = false; + while let Some(rec) = logger.pop() { + if rec.args().contains("status:500") { + found = true; + break; + } + } + assert!(found, "Expected log to contain HTTP 500 status"); + } + + #[tokio::test] + #[serial] + async fn should_handle_invalid_chain_task_id_in_url() { + { + let mut logger = TEST_LOGGER.lock().unwrap(); + while logger.pop().is_some() {} + } + let mock_server = MockServer::start().await; + let server_uri = mock_server.uri(); + + let invalid_chain_task_id = "invalidTaskId"; + let computed_file = ComputedFile { + task_id: Some(invalid_chain_task_id.to_string()), + ..Default::default() + }; + + let result = tokio::task::spawn_blocking(move || { + let client = WorkerApiClient::new(&server_uri); + client.send_computed_file_to_host(CHALLENGE, invalid_chain_task_id, &computed_file) + }) + .await + .expect("Task panicked"); + + assert!(result.is_err(), "Should fail with invalid chain task ID"); + if let Err(error) = result { + assert_eq!( + error, + ReplicateStatusCause::PostComputeSendComputedFileFailed, + "Expected PostComputeSendComputedFileFailed, got: {error:?}" + ); + } + let mut logger = TEST_LOGGER.lock().unwrap(); + let mut found = false; + while let Some(rec) = logger.pop() { + if rec.args().contains("status:404") { + found = true; + break; + } + } + assert!(found, "Expected log to contain HTTP 404 status"); + } + + #[tokio::test] + async fn should_send_computed_file_with_minimal_data() { + let mock_server = MockServer::start().await; + let server_uri = mock_server.uri(); + + let computed_file = ComputedFile { + task_id: Some(CHAIN_TASK_ID.to_string()), + ..Default::default() + }; + + let expected_path = format!("/compute/post/{CHAIN_TASK_ID}/computed"); + let expected_body = json!(computed_file); + + Mock::given(method("POST")) + .and(path(expected_path.as_str())) + .and(header("Authorization", CHALLENGE)) + .and(body_json(&expected_body)) + .respond_with(ResponseTemplate::new(200)) + .expect(1) + .mount(&mock_server) + .await; + + let result = tokio::task::spawn_blocking(move || { + let client = WorkerApiClient::new(&server_uri); + client.send_computed_file_to_host(CHALLENGE, CHAIN_TASK_ID, &computed_file) + }) + .await + .expect("Task panicked"); + + assert!(result.is_ok()); + } + // endregion +} diff --git a/post-compute/src/bin/tee_worker_post_compute.rs b/post-compute/src/bin/tee_worker_post_compute.rs new file mode 100644 index 0000000..fb9d01e --- /dev/null +++ b/post-compute/src/bin/tee_worker_post_compute.rs @@ -0,0 +1,10 @@ +use env_logger::{Builder, Env, Target}; +use std::process; +use tee_worker_post_compute::compute::app_runner::start; + +fn main() { + Builder::from_env(Env::default().default_filter_or("info")) + .target(Target::Stdout) + .init(); + process::exit(start() as i32); +} diff --git a/post-compute/src/compute.rs b/post-compute/src/compute.rs new file mode 100644 index 0000000..b761c76 --- /dev/null +++ b/post-compute/src/compute.rs @@ -0,0 +1,8 @@ +pub mod app_runner; +pub mod computed_file; +pub mod dropbox; +pub mod encryption; +pub mod errors; +pub mod signer; +pub mod utils; +pub mod web2_result; diff --git a/post-compute/src/compute/app_runner.rs b/post-compute/src/compute/app_runner.rs new file mode 100644 index 0000000..da70cec --- /dev/null +++ b/post-compute/src/compute/app_runner.rs @@ -0,0 +1,595 @@ +use crate::api::worker_api::{ExitMessage, WorkerApiClient}; +use crate::compute::{ + computed_file::{ + ComputedFile, build_result_digest_in_computed_file, read_computed_file, sign_computed_file, + }, + errors::ReplicateStatusCause, + signer::get_challenge, + utils::env_utils::{TeeSessionEnvironmentVariable, get_env_var_or_error}, + web2_result::{Web2ResultInterface, Web2ResultService}, +}; +use log::{error, info}; + +/// Represents the different exit modes for a process or application. +/// +/// Each variant is explicitly assigned an `i32` value, and the enum +/// uses `#[repr(i32)]` to ensure its memory representation matches C-style enums. +#[cfg_attr(test, derive(Debug, PartialEq))] +#[repr(i32)] +pub enum ExitMode { + Success = 0, + ReportedFailure = 1, + UnreportedFailure = 2, + InitializationFailure = 3, +} + +/// Defines the interface for post-compute operations. +/// +/// This trait encapsulates the core functionality needed for running post-compute operations. +/// Implementations of this trait can be used with the [`start_with_runner`] function to execute +/// the post-compute workflow. +pub trait PostComputeRunnerInterface { + fn run_post_compute(&self, chain_task_id: &str) -> Result<(), ReplicateStatusCause>; + fn get_challenge(&self, chain_task_id: &str) -> Result; + fn send_exit_cause( + &self, + authorization: &str, + chain_task_id: &str, + exit_message: &ExitMessage, + ) -> Result<(), ReplicateStatusCause>; + fn send_computed_file(&self, computed_file: &ComputedFile) -> Result<(), ReplicateStatusCause>; +} + +/// Production implementation of [`PostComputeRunnerInterface`] +/// +/// This struct provides a concrete implementation of the [`PostComputeRunnerInterface`], +/// using the [`super::signer`] module for challenge generation and the owned [`WorkerApiClient`] +/// instance for error reporting. +pub struct DefaultPostComputeRunner { + worker_api_client: WorkerApiClient, +} + +#[allow( + clippy::new_without_default, + reason = "The new method will be replaced by the recommended Default trait implementation later" +)] +impl DefaultPostComputeRunner { + pub fn new() -> Self { + Self { + worker_api_client: WorkerApiClient::from_env(), + } + } +} + +impl PostComputeRunnerInterface for DefaultPostComputeRunner { + fn run_post_compute(&self, chain_task_id: &str) -> Result<(), ReplicateStatusCause> { + let should_callback: bool = match get_env_var_or_error( + TeeSessionEnvironmentVariable::ResultStorageCallback, + ReplicateStatusCause::PostComputeFailedUnknownIssue, //TODO: Update this error cause to a more specific one + ) { + Ok(value) => match value.to_lowercase().parse::() { + Ok(parsed_value) => parsed_value, + Err(_) => { + error!( + "Failed to parse RESULT_STORAGE_CALLBACK environment variable as a boolean [callback_env_var:{value}]" + ); + return Err(ReplicateStatusCause::PostComputeFailedUnknownIssue); + } + }, + Err(e) => { + error!("Failed to get RESULT_STORAGE_CALLBACK environment variable"); + return Err(e); + } + }; + + let mut computed_file = read_computed_file(chain_task_id, "/iexec_out")?; + build_result_digest_in_computed_file(&mut computed_file, should_callback)?; + sign_computed_file(&mut computed_file)?; + + if !should_callback { + Web2ResultService.encrypt_and_upload_result(&computed_file)?; + } + + self.send_computed_file(&computed_file)?; + + Ok(()) + } + + fn get_challenge(&self, chain_task_id: &str) -> Result { + get_challenge(chain_task_id) + } + + fn send_exit_cause( + &self, + authorization: &str, + chain_task_id: &str, + exit_message: &ExitMessage, + ) -> Result<(), ReplicateStatusCause> { + self.worker_api_client + .send_exit_cause_for_post_compute_stage(authorization, chain_task_id, exit_message) + } + + fn send_computed_file(&self, computed_file: &ComputedFile) -> Result<(), ReplicateStatusCause> { + info!("send_computed_file stage started [computedFile:{computed_file:#?}]"); + let task_id = match computed_file.task_id.as_ref() { + Some(id) => id, + None => { + error!("send_computed_file stage failed: task_id is missing in computed file"); + return Err(ReplicateStatusCause::PostComputeFailedUnknownIssue); + } + }; + let authorization = self.get_challenge(task_id)?; + match self.worker_api_client.send_computed_file_to_host( + &authorization, + task_id, + computed_file, + ) { + Ok(_) => { + info!("send_computed_file stage completed"); + Ok(()) + } + Err(_) => { + error!("send_computed_file stage failed [task_id:{task_id}]"); + Err(ReplicateStatusCause::PostComputeSendComputedFileFailed) + } + } + } +} + +/// Executes the post-compute workflow with a provided runner implementation. +/// +/// This function orchestrates the full post-compute process, handling environment +/// variable checks, execution of the main post-compute logic, and error reporting. +/// It uses the provided runner to execute core operations and handles all the +/// workflow states and transitions. +/// +/// # Arguments +/// +/// * `runner` - An implementation of [`PostComputeRunnerInterface`] that will be used to execute the post-compute operations. +/// +/// # Example +/// +/// ```rust +/// use tee_worker_post_compute::compute::app_runner::{ +/// start_with_runner, +/// DefaultPostComputeRunner, +/// }; +/// +/// // Using the default runner +/// let runner = DefaultPostComputeRunner::new(); +/// let exit_code = start_with_runner(&runner); +/// ``` +pub fn start_with_runner(runner: &R) -> ExitMode { + println!("Tee worker post-compute started"); + let chain_task_id: String = match get_env_var_or_error( + TeeSessionEnvironmentVariable::IexecTaskId, + ReplicateStatusCause::PostComputeTaskIdMissing, + ) { + Ok(id) => id, + Err(e) => { + error!( + "TEE post-compute cannot go further without taskID context [errorMessage:{e:?}]" + ); + return ExitMode::InitializationFailure; + } + }; + match runner.run_post_compute(&chain_task_id) { + Ok(()) => { + info!("TEE post-compute completed"); + ExitMode::Success + } + Err(exit_cause) => { + error!("TEE post-compute failed with exit cause [errorMessage:{exit_cause}]"); + + let authorization: String = match runner.get_challenge(&chain_task_id) { + Ok(challenge) => challenge, + Err(_) => { + error!("Failed to retrieve authorization [taskId:{chain_task_id}]"); + return ExitMode::UnreportedFailure; + } + }; + + let exit_message = ExitMessage::from(&exit_cause); + + match runner.send_exit_cause(&authorization, &chain_task_id, &exit_message) { + Ok(()) => ExitMode::ReportedFailure, + Err(_) => { + error!("Failed to report exit cause [exitCause:{exit_cause}]"); + ExitMode::UnreportedFailure + } + } + } + } +} + +/// Starts the post-compute process using the [`DefaultPostComputeRunner`]. +/// +/// This is a convenience function that creates a [`DefaultPostComputeRunner`] +/// and passes it to [`start_with_runner`]. +/// +/// # Returns +/// +/// * `i32` - An exit code indicating the result of the post-compute process. +/// See [`start_with_runner`] for details on the possible exit codes. +/// +/// # Example +/// +/// ```rust +/// use tee_worker_post_compute::compute::app_runner::{start, ExitMode}; +/// +/// let exit_mode = start(); +/// match exit_mode { +/// ExitMode::Success => println!("Post-compute completed successfully"), +/// ExitMode::ReportedFailure => println!("Post-compute failed (reported)"), +/// ExitMode::UnreportedFailure => println!("Post-compute failed (unreported)"), +/// ExitMode::InitializationFailure => println!("Post-compute initialization failed"), +/// } +/// ``` +pub fn start() -> ExitMode { + let runner = DefaultPostComputeRunner::new(); + start_with_runner(&runner) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::compute::{ + computed_file::ComputedFile, errors::ReplicateStatusCause, + utils::env_utils::TeeSessionEnvironmentVariable, + }; + use temp_env::with_vars; + use wiremock::{ + Mock, MockServer, ResponseTemplate, + matchers::{header, method, path}, + }; + + struct MockRunner { + run_post_compute_success: bool, + get_challenge_success: bool, + send_exit_cause_success: bool, + send_computed_file_success: bool, + error_cause: Option, + } + + impl MockRunner { + fn new() -> Self { + Self { + run_post_compute_success: true, + get_challenge_success: true, + send_exit_cause_success: true, + send_computed_file_success: true, + error_cause: None, + } + } + + fn with_run_post_compute_failure(mut self, cause: Option) -> Self { + self.run_post_compute_success = false; + self.error_cause = cause; + self + } + + fn with_get_challenge_failure(mut self) -> Self { + self.get_challenge_success = false; + self + } + + fn with_send_exit_cause_failure(mut self) -> Self { + self.send_exit_cause_success = false; + self + } + } + + impl PostComputeRunnerInterface for MockRunner { + fn run_post_compute(&self, _chain_task_id: &str) -> Result<(), ReplicateStatusCause> { + if self.run_post_compute_success { + Ok(()) + } else if let Some(cause) = &self.error_cause { + Err(cause.clone()) + } else { + Err(ReplicateStatusCause::PostComputeFailedUnknownIssue) + } + } + + fn get_challenge(&self, _chain_task_id: &str) -> Result { + if self.get_challenge_success { + Ok("mock_challenge".to_string()) + } else { + Err(ReplicateStatusCause::PostComputeTeeChallengePrivateKeyMissing) + } + } + + fn send_exit_cause( + &self, + _authorization: &str, + _chain_task_id: &str, + _exit_message: &ExitMessage, + ) -> Result<(), ReplicateStatusCause> { + if self.send_exit_cause_success { + Ok(()) + } else { + Err(ReplicateStatusCause::PostComputeFailedUnknownIssue) + } + } + + fn send_computed_file( + &self, + _exit_message: &ComputedFile, + ) -> Result<(), ReplicateStatusCause> { + if self.send_computed_file_success { + Ok(()) + } else { + Err(ReplicateStatusCause::PostComputeSendComputedFileFailed) + } + } + } + + const TEST_TASK_ID: &str = "0x1234567890abcdef"; + + // region start + #[test] + fn start_return_3_when_task_id_missing() { + with_vars( + vec![( + TeeSessionEnvironmentVariable::IexecTaskId.name(), + None::<&str>, + )], + || { + let runner = MockRunner::new(); + let result = start_with_runner(&runner); + assert_eq!( + result, + ExitMode::InitializationFailure, + "Should return ExitMode::InitializationFailure when chain task ID is missing" + ); + }, + ); + } + + #[test] + fn start_return_3_when_empty_task_id() { + with_vars( + vec![(TeeSessionEnvironmentVariable::IexecTaskId.name(), Some(""))], + || { + let runner = MockRunner::new(); + let result = start_with_runner(&runner); + assert_eq!( + result, + ExitMode::InitializationFailure, + "Should return ExitMode::InitializationFailure when chain task ID is empty" + ); + }, + ); + } + + #[test] + fn start_return_0_when_successful() { + with_vars( + vec![( + TeeSessionEnvironmentVariable::IexecTaskId.name(), + Some(TEST_TASK_ID), + )], + || { + let runner = MockRunner::new(); + let result = start_with_runner(&runner); + assert_eq!( + result, + ExitMode::Success, + "Should return ExitMode::Success on successful execution" + ); + }, + ); + } + + #[test] + fn start_return_1_when_fail_with_known_cause() { + with_vars( + vec![( + TeeSessionEnvironmentVariable::IexecTaskId.name(), + Some(TEST_TASK_ID), + )], + || { + let runner = MockRunner::new().with_run_post_compute_failure(Some( + ReplicateStatusCause::PostComputeInvalidTeeSignature, + )); + + let result = start_with_runner(&runner); + assert_eq!( + result, + ExitMode::ReportedFailure, + "Should return ExitMode::ReportedFailure when error is reported successfully" + ); + }, + ); + } + + #[test] + fn start_return_1_when_fail_with_unknown_cause() { + with_vars( + vec![( + TeeSessionEnvironmentVariable::IexecTaskId.name(), + Some(TEST_TASK_ID), + )], + || { + let runner = MockRunner::new().with_run_post_compute_failure(None); + + let result = start_with_runner(&runner); + assert_eq!( + result, + ExitMode::ReportedFailure, + "Should return ExitMode::ReportedFailure when unknown error is reported successfully" + ); + }, + ); + } + + #[test] + fn start_return_2_when_exit_cause_not_transmitted() { + with_vars( + vec![( + TeeSessionEnvironmentVariable::IexecTaskId.name(), + Some(TEST_TASK_ID), + )], + || { + let runner = MockRunner::new() + .with_run_post_compute_failure(Some( + ReplicateStatusCause::PostComputeInvalidTeeSignature, + )) + .with_send_exit_cause_failure(); + + let result = start_with_runner(&runner); + assert_eq!( + result, + ExitMode::UnreportedFailure, + "Should return ExitMode::UnreportedFailure when error reporting fails" + ); + }, + ); + } + + #[test] + fn start_return_2_when_get_challenge_fails() { + with_vars( + vec![( + TeeSessionEnvironmentVariable::IexecTaskId.name(), + Some(TEST_TASK_ID), + )], + || { + let runner = MockRunner::new() + .with_run_post_compute_failure(Some( + ReplicateStatusCause::PostComputeInvalidTeeSignature, + )) + .with_get_challenge_failure(); + + let result = start_with_runner(&runner); + assert_eq!( + result, + ExitMode::UnreportedFailure, + "Should return ExitMode::UnreportedFailure when signer service fails" + ); + }, + ); + } + // endregion + + // region send_computed_file + const TEST_WORKER_ADDRESS: &str = "0x1234567890abcdef1234567890abcdef12345678"; + const TEST_PRIVATE_KEY: &str = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + const TEST_CHALLENGE: &str = "0x184afe6f0d4232c37623d203f4ec42b8281bd7a7f3655c66e65b23b7dbac266330db02efc9bc1bd682405cc1b8876806e086729e1ef7f880e5782aade94cd5741c"; + + fn create_test_computed_file(task_id: Option) -> ComputedFile { + ComputedFile { + task_id, + result_digest: Some("0xresultdigest".to_string()), + enclave_signature: Some("0xsignature".to_string()), + deterministic_output_path: Some("/path/to/output".to_string()), + callback_data: None, + error_message: None, + } + } + + async fn send_compute_file_action(server_url: String) -> Result<(), ReplicateStatusCause> { + tokio::task::spawn_blocking(move || { + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::SignWorkerAddress.name(), + Some(TEST_WORKER_ADDRESS), + ), + ( + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey.name(), + Some(TEST_PRIVATE_KEY), + ), + ( + TeeSessionEnvironmentVariable::WorkerHostEnvVar.name(), + Some(&server_url.replace("http://", "")), + ), + ], + || { + let runner = DefaultPostComputeRunner::new(); + let computed_file = create_test_computed_file(Some(TEST_TASK_ID.to_string())); + runner.send_computed_file(&computed_file) + }, + ) + }) + .await + .expect("Task panicked") + } + + #[tokio::test] + async fn test_send_computed_file_success() { + let mock_server = MockServer::start().await; + let server_url = mock_server.uri(); + + Mock::given(method("POST")) + .and(path(format!("/compute/post/{TEST_TASK_ID}/computed"))) + .and(header("Authorization", TEST_CHALLENGE)) + .respond_with(ResponseTemplate::new(200)) + .expect(1) + .mount(&mock_server) + .await; + + let result = send_compute_file_action(server_url).await; + assert!(result.is_ok(), "send_computed_file should succeed"); + } + + #[test] + fn send_computed_file_fails_when_task_id_missing() { + let runner = DefaultPostComputeRunner::new(); + let computed_file = create_test_computed_file(None); + + let result = runner.send_computed_file(&computed_file); + + assert!(result.is_err(), "Should fail when task_id is missing"); + assert_eq!( + result.unwrap_err(), + ReplicateStatusCause::PostComputeFailedUnknownIssue, + "Should return PostComputeFailedUnknownIssue when task_id is missing" + ); + } + + #[test] + fn send_computed_file_fails_when_get_challenge_fails() { + with_vars( + vec![( + TeeSessionEnvironmentVariable::SignWorkerAddress.name(), + None::<&str>, + )], + || { + let runner = DefaultPostComputeRunner::new(); + let computed_file = create_test_computed_file(Some(TEST_TASK_ID.to_string())); + let result = runner.send_computed_file(&computed_file); + + assert!(result.is_err(), "Should fail when get_challenge fails"); + assert_eq!( + result.unwrap_err(), + ReplicateStatusCause::PostComputeWorkerAddressMissing, + "Should propagate the error from get_challenge" + ); + }, + ); + } + + #[tokio::test] + async fn send_computed_file_fails_when_http_failrue() { + let mock_server = MockServer::start().await; + let server_url = mock_server.uri(); + + Mock::given(method("POST")) + .and(path(format!("/compute/post/{TEST_TASK_ID}/computed"))) + .and(header("Authorization", TEST_CHALLENGE)) + .respond_with(ResponseTemplate::new(500)) + .expect(1) + .mount(&mock_server) + .await; + + let result = send_compute_file_action(server_url).await; + assert!(result.is_err(), "Should fail when HTTP request fails"); + assert_eq!( + result.unwrap_err(), + ReplicateStatusCause::PostComputeSendComputedFileFailed, + "Should return PostComputeSendComputedFileFailed when HTTP request fails" + ); + } + // endregion +} diff --git a/post-compute/src/compute/computed_file.rs b/post-compute/src/compute/computed_file.rs new file mode 100644 index 0000000..c374412 --- /dev/null +++ b/post-compute/src/compute/computed_file.rs @@ -0,0 +1,693 @@ +use crate::compute::{ + errors::ReplicateStatusCause, + signer::sign_enclave_challenge, + utils::{ + env_utils::{TeeSessionEnvironmentVariable, get_env_var_or_error}, + hash_utils::concatenate_and_hash, + result_utils::{compute_web2_result_digest, compute_web3_result_digest}, + }, +}; +use log::{error, info}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::Path}; + +/// Represents the structure of a computed.json file generated by iExec tasks. +/// +/// This struct contains all the necessary information about a completed computation task, +/// including output paths, callback data, task identification, and result verification data. +/// The fields are serialized/deserialized using camelCase naming convention to match +/// the expected JSON format. +/// +/// # Example +/// +/// ```json +/// { +/// "deterministic-output-path": "/iexec_out/result.txt", +/// "callback-data": "0xabc123...", +/// "task-id": "0x123456789abcdef", +/// "result-digest": "0x789abc...", +/// "enclave-signature": "0xdef456...", +/// "error-message": null +/// } +/// ``` +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct ComputedFile { + pub deterministic_output_path: Option, + pub callback_data: Option, + pub task_id: Option, + pub result_digest: Option, + pub enclave_signature: Option, + pub error_message: Option, +} + +/// Reads and parses a computed.json file from the specified directory. +/// +/// This function locates the computed.json file in the given directory, reads its contents, +/// and deserializes it into a [`ComputedFile`] struct. The task ID is automatically set +/// in the resulting struct instance. +/// +/// # Arguments +/// +/// * `chain_task_id` - The blockchain task identifier to associate with this computed file +/// * `computed_file_dir` - The directory path where the computed.json file is located +/// +/// # Returns +/// +/// * `Ok(ComputedFile)` - Successfully parsed computed file with task ID set +/// * `Err(ReplicateStatusCause)` - Error if file cannot be read or parsed +/// +/// # Errors +/// +/// This function will return an error in the following situations: +/// * `chain_task_id` is empty (returns `PostComputeComputedFileNotFound`) +/// * `computed_file_dir` is empty (returns `PostComputeComputedFileNotFound`) +/// * The computed.json file does not exist in the specified directory (returns `PostComputeComputedFileNotFound`) +/// * The file cannot be read due to permissions or I/O errors (returns `PostComputeComputedFileNotFound`) +/// * The JSON content is invalid or cannot be deserialized (returns `PostComputeComputedFileNotFound`) +/// +/// # Example +/// +/// ``` +/// use tee_worker_post_compute::compute::computed_file::read_computed_file; +/// +/// match read_computed_file("0x123456789abcdef", "/iexec_out") { +/// Ok(computed_file) => { +/// println!("Task ID: {:?}", computed_file.task_id); +/// println!("Output path: {:?}", computed_file.deterministic_output_path); +/// }, +/// Err(e) => eprintln!("Error reading computed file: {:?}", e), +/// } +/// ``` +pub fn read_computed_file( + chain_task_id: &str, + computed_file_dir: &str, +) -> Result { + info!("read_computed_file stage started"); + if chain_task_id.is_empty() { + error!( + "Failed to read compute file (empty chain_task_id) [chain_task_id:{chain_task_id}, computed_file_dir:{computed_file_dir}]" + ); + return Err(ReplicateStatusCause::PostComputeComputedFileNotFound); + } + + if computed_file_dir.is_empty() { + error!( + "Failed to read compute file (empty computed_file_dir) [chain_task_id:{chain_task_id}, computed_file_dir:{computed_file_dir}]" + ); + return Err(ReplicateStatusCause::PostComputeComputedFileNotFound); + } + + let computed_file_path = Path::new(computed_file_dir).join("computed.json"); + let json_string = match fs::read_to_string(&computed_file_path) { + Ok(content) => content, + Err(e) => { + error!( + "Failed to read compute file [chain_task_id:{chain_task_id}, computed_file_dir:{computed_file_dir}, error:{e}]" + ); + return Err(ReplicateStatusCause::PostComputeComputedFileNotFound); + } + }; + + match serde_json::from_str::(&json_string) { + Ok(mut computed_file) => { + computed_file.task_id = Some(chain_task_id.to_string()); + info!("read_computed_file stage completed"); + Ok(computed_file) + } + Err(_) => { + error!( + "Failed to read compute file [chain_task_id:{chain_task_id}, computed_file_dir:{computed_file_dir}]" + ); + Err(ReplicateStatusCause::PostComputeComputedFileNotFound) + } + } +} + +/// Computes and sets the result digest for a computed file based on the task type. +/// +/// This function determines the appropriate digest computation method based on whether +/// the task is in callback mode (web3) or standard mode (web2), then computes and +/// stores the result digest in the provided [`ComputedFile`] instance. +/// +/// The digest computation follows these rules: +/// - **Web3 mode** (callback): Uses keccak256 hash of the callback data +/// - **Web2 mode** (standard): Uses SHA256 hash of the output files/directory +/// +/// # Arguments +/// +/// * `computed_file` - A mutable reference to the [`ComputedFile`] instance to update +/// * `is_callback_mode` - Boolean indicating whether this is a web3 callback task +/// +/// # Returns +/// +/// * `Ok(())` - Successfully computed and set the result digest +/// * `Err(ReplicateStatusCause)` - Error if digest computation failed +/// +/// # Errors +/// +/// This function will return an error in the following situations: +/// * The result digest computation returns an empty string (returns `PostComputeResultDigestComputationFailed`) +/// * For web3 mode: callback data is missing or empty +/// * For web2 mode: deterministic output path is missing, empty, or points to non-existent files +/// +/// # Example +/// +/// ``` +/// use tee_worker_post_compute::compute::computed_file::{ +/// build_result_digest_in_computed_file, +/// ComputedFile +/// }; +/// +/// let mut computed_file = ComputedFile { +/// task_id: Some("0x123".to_string()), +/// callback_data: Some("0xabc...".to_string()), +/// deterministic_output_path: None, +/// result_digest: None, +/// enclave_signature: None, +/// error_message: None, +/// }; +/// +/// // For a web3 callback task +/// match build_result_digest_in_computed_file(&mut computed_file, true) { +/// Ok(()) => println!("Result digest: {:?}", computed_file.result_digest), +/// Err(e) => eprintln!("Error computing digest: {:?}", e), +/// } +/// ``` +pub fn build_result_digest_in_computed_file( + computed_file: &mut ComputedFile, + is_callback_mode: bool, +) -> Result<(), ReplicateStatusCause> { + info!( + "build_result_digest_in_computed_file stage started [mode:{}]", + if is_callback_mode { "web3" } else { "web2" } + ); + + let result_digest = if is_callback_mode { + compute_web3_result_digest(computed_file) + } else { + compute_web2_result_digest(computed_file) + }; + + if result_digest.is_empty() { + return Err(ReplicateStatusCause::PostComputeResultDigestComputationFailed); + } + + computed_file.result_digest = Some(result_digest.to_string()); + Ok(()) +} + +/// Signs the computed file with the enclave signature. +/// +/// This function generates a cryptographic signature for the computed file to ensure +/// its integrity and authenticity. The signature is created by: +/// 1. Computing a result hash from the task ID and result digest +/// 2. Computing a result seal from the worker address, task ID, and result digest +/// 3. Combining these to create a message hash +/// 4. Signing the message hash with the TEE challenge private key +/// +/// The generated signature is stored in the `enclave_signature` field of the computed file. +/// +/// # Arguments +/// +/// * `computed_file` - A mutable reference to the [`ComputedFile`] to be signed +/// +/// # Returns +/// +/// * `Ok(())` - Successfully generated and stored the enclave signature +/// * `Err(ReplicateStatusCause)` - Error if signing failed due to: +/// - Missing worker address environment variable +/// - Missing TEE challenge private key environment variable +/// - Invalid private key format +/// - Signing operation failure +/// +/// # Panics +/// +/// This function will panic if: +/// * `computed_file.task_id` is `None` +/// * `computed_file.result_digest` is `None` +/// +/// # Environment Variables +/// +/// Required environment variables: +/// * `SIGN_WORKER_ADDRESS` - The worker's address used in the result seal computation +/// * `SIGN_TEE_CHALLENGE_PRIVATE_KEY` - The private key used for signing +/// +/// # Example +/// +/// ``` +/// use tee_worker_post_compute::compute::computed_file::{ +/// sign_computed_file, +/// ComputedFile +/// }; +/// +/// // Assuming environment variables are set: +/// // SIGN_WORKER_ADDRESS=0x1234567890abcdef1234567890abcdef12345678 +/// // SIGN_TEE_CHALLENGE_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +/// +/// let mut computed_file = ComputedFile { +/// task_id: Some("0x123456789abcdef".to_string()), +/// result_digest: Some("0xcb371be217faa47dab94e0d0ff0840c6cbf41645f0dc1a6ae3f34447155a76f3".to_string()), +/// ..Default::default() +/// }; +/// +/// match sign_computed_file(&mut computed_file) { +/// Ok(()) => { +/// println!("Signature: {:?}", computed_file.enclave_signature); +/// assert!(computed_file.enclave_signature.is_some()); +/// }, +/// Err(e) => eprintln!("Signing failed: {:?}", e), +/// } +/// ``` +/// +/// # Security Considerations +/// +/// The enclave signature provides cryptographic proof that the computation was performed +/// by an authorized TEE enclave. The signature includes the worker address to prevent +/// replay attacks and ensure the result is bound to a specific worker. +pub fn sign_computed_file(computed_file: &mut ComputedFile) -> Result<(), ReplicateStatusCause> { + info!("Signer stage started"); + let task_id = computed_file + .task_id + .as_ref() + .ok_or(ReplicateStatusCause::PostComputeTaskIdMissing)?; + let result_digest = computed_file + .result_digest + .as_ref() + .ok_or(ReplicateStatusCause::PostComputeResultDigestComputationFailed)?; + + let worker_address: String = get_env_var_or_error( + TeeSessionEnvironmentVariable::SignWorkerAddress, + ReplicateStatusCause::PostComputeWorkerAddressMissing, + )?; + + let result_hash = concatenate_and_hash(&[task_id, result_digest]); + let result_seal = concatenate_and_hash(&[&worker_address, task_id, result_digest]); + let message_hash = concatenate_and_hash(&[&result_hash, &result_seal]); + + let tee_challenge_private_key: String = get_env_var_or_error( + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey, + ReplicateStatusCause::PostComputeTeeChallengePrivateKeyMissing, + )?; + + let enclave_signature = sign_enclave_challenge(&message_hash, &tee_challenge_private_key)?; + + computed_file.enclave_signature = Some(enclave_signature); + info!("Signer stage completed"); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use temp_env::with_vars; + use tempfile::tempdir; + + const TEST_TASK_ID: &str = "0x123456789abcdef"; + + #[test] + fn computed_file_serializes_to_kebab_case() { + let file = ComputedFile { + deterministic_output_path: Some("/iexec_out/result.txt".to_string()), + callback_data: Some("0xabc".to_string()), + task_id: Some(TEST_TASK_ID.to_string()), + result_digest: Some("0xdef".to_string()), + enclave_signature: Some("0xsig".to_string()), + error_message: Some("err".to_string()), + }; + let expected = r#"{ + "deterministic-output-path":"/iexec_out/result.txt", + "callback-data":"0xabc", + "task-id":"0x123456789abcdef", + "result-digest":"0xdef", + "enclave-signature":"0xsig", + "error-message":"err" + }"#; + let expected_value: serde_json::Value = serde_json::from_str(expected).unwrap(); + let actual_value: serde_json::Value = serde_json::json!(&file); + assert_eq!(actual_value, expected_value); + } + + // region read_computed_file + #[test] + fn read_computed_file_returns_computed_file_when_valid_input() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_str().unwrap(); + let file_path = dir.path().join("computed.json"); + + let test_json = + r#"{"deterministic-output-path":"/iexec_out/result.txt","callback-data":"0xabc"}"#; + let mut file = fs::File::create(&file_path).unwrap(); + file.write_all(test_json.as_bytes()).unwrap(); + + let result = read_computed_file(TEST_TASK_ID, dir_path); + assert!(result.is_ok()); + + let computed_file = result.unwrap(); + assert_eq!(computed_file.task_id, Some(TEST_TASK_ID.to_string())); + assert_eq!( + computed_file.deterministic_output_path, + Some("/iexec_out/result.txt".to_string()) + ); + assert_eq!(computed_file.callback_data, Some("0xabc".to_string())); + } + + #[test] + fn read_computed_file_returns_error_when_chain_task_id_is_empty() { + let result = read_computed_file("", "/tmp"); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + ReplicateStatusCause::PostComputeComputedFileNotFound + ); + } + + #[test] + fn read_computed_file_returns_error_when_computed_file_dir_is_empty() { + let result = read_computed_file(TEST_TASK_ID, ""); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + ReplicateStatusCause::PostComputeComputedFileNotFound + ); + } + + #[test] + fn read_computed_file_returns_error_when_computed_json_is_missing() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_str().unwrap(); + + let result = read_computed_file(TEST_TASK_ID, dir_path); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + ReplicateStatusCause::PostComputeComputedFileNotFound + ); + } + + #[test] + fn read_computed_file_returns_error_when_computed_json_is_empty() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("computed.json"); + + let test_json = ""; + let mut file = fs::File::create(&file_path).unwrap(); + file.write_all(test_json.as_bytes()).unwrap(); + + let result = read_computed_file(TEST_TASK_ID, dir.path().to_str().unwrap()); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + ReplicateStatusCause::PostComputeComputedFileNotFound + ); + } + + #[test] + fn read_computed_file_returns_error_when_computed_json_is_invalid() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("computed.json"); + + let test_json = r#"{"invalid-json":}"#; + let mut file = fs::File::create(&file_path).unwrap(); + file.write_all(test_json.as_bytes()).unwrap(); + + let result = read_computed_file(TEST_TASK_ID, dir.path().to_str().unwrap()); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + ReplicateStatusCause::PostComputeComputedFileNotFound + ); + } + // endregion + + // region build_result_digest_in_computed_file + #[test] + fn build_result_digest_in_computed_file_computes_web3_digest_when_is_callback_mode_is_true() { + let mut computed_file = ComputedFile { + task_id: Some(TEST_TASK_ID.to_string()), + callback_data: Some( + "0x0000000000000000000000000000000000000000000000000000000000000001".to_string(), + ), + ..Default::default() + }; + + let result = build_result_digest_in_computed_file(&mut computed_file, true); + + assert!(result.is_ok()); + assert_eq!( + computed_file.result_digest, + Some("0xcb371be217faa47dab94e0d0ff0840c6cbf41645f0dc1a6ae3f34447155a76f3".to_string()) + ); + } + + #[test] + fn build_result_digest_in_computed_file_computes_web2_digest_when_is_callback_mode_is_false() { + let dir = tempdir().unwrap(); + let output_dir = dir.path().join("output"); + fs::create_dir(&output_dir).unwrap(); + + let test_file_path = output_dir.join("test.txt"); + let mut file = fs::File::create(&test_file_path).unwrap(); + file.write_all(b"test content").unwrap(); + + let mut computed_file = ComputedFile { + task_id: Some(TEST_TASK_ID.to_string()), + deterministic_output_path: Some(output_dir.to_str().unwrap().to_string()), + ..Default::default() + }; + + let result = build_result_digest_in_computed_file(&mut computed_file, false); + + assert!(result.is_ok()); + assert!(computed_file.result_digest.is_some()); + } + + #[test] + fn build_result_digest_in_computed_file_returns_error_when_result_digest_is_empty() { + let mut computed_file = ComputedFile { + task_id: Some(TEST_TASK_ID.to_string()), + deterministic_output_path: Some("/non_existent_path".to_string()), + ..Default::default() + }; + + let result = build_result_digest_in_computed_file(&mut computed_file, false); + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + ReplicateStatusCause::PostComputeResultDigestComputationFailed + ); + } + // endregion + + // region sign_computed_file + const TEST_WORKER_ADDRESS: &str = "0x1234567890abcdef1234567890abcdef12345678"; + const TEST_TEE_CHALLENGE_PRIVATE_KEY: &str = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + const TEST_RESULT_DIGEST: &str = + "0xcb371be217faa47dab94e0d0ff0840c6cbf41645f0dc1a6ae3f34447155a76f3"; + + fn make_computed_file( + task_id: Option<&str>, + callback_data: Option<&str>, + deterministic_output_path: Option<&str>, + result_digest: Option<&str>, + enclave_signature: Option<&str>, + error_message: Option<&str>, + ) -> ComputedFile { + ComputedFile { + task_id: task_id.map(|s| s.to_string()), + callback_data: callback_data.map(|s| s.to_string()), + deterministic_output_path: deterministic_output_path.map(|s| s.to_string()), + result_digest: result_digest.map(|s| s.to_string()), + enclave_signature: enclave_signature.map(|s| s.to_string()), + error_message: error_message.map(|s| s.to_string()), + } + } + + #[test] + fn sign_computed_file_returns_signature_when_all_env_and_fields_present() { + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::SignWorkerAddress.name(), + Some(TEST_WORKER_ADDRESS), + ), + ( + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey.name(), + Some(TEST_TEE_CHALLENGE_PRIVATE_KEY), + ), + ], + || { + let mut computed_file = make_computed_file( + Some(TEST_TASK_ID), + None, + None, + Some(TEST_RESULT_DIGEST), + None, + None, + ); + let result = sign_computed_file(&mut computed_file); + assert!(result.is_ok(), "Signing should be successful"); + assert!( + computed_file.enclave_signature.is_some(), + "Enclave signature should be Some" + ); + assert!( + !computed_file.enclave_signature.as_ref().unwrap().is_empty(), + "Enclave signature should not be empty" + ); + }, + ); + } + + #[test] + fn sign_computed_file_returns_error_when_worker_address_missing() { + with_vars( + vec![( + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey.name(), + Some(TEST_TEE_CHALLENGE_PRIVATE_KEY), + )], + || { + let mut computed_file = make_computed_file( + Some(TEST_TASK_ID), + None, + None, + Some(TEST_RESULT_DIGEST), + None, + None, + ); + let result = sign_computed_file(&mut computed_file); + assert!( + matches!( + result, + Err(ReplicateStatusCause::PostComputeWorkerAddressMissing) + ), + "Should return PostComputeWorkerAddressMissing error" + ); + }, + ); + } + + #[test] + fn sign_computed_file_returns_error_when_tee_private_key_missing() { + with_vars( + vec![( + TeeSessionEnvironmentVariable::SignWorkerAddress.name(), + Some(TEST_WORKER_ADDRESS), + )], + || { + let mut computed_file = make_computed_file( + Some(TEST_TASK_ID), + None, + None, + Some(TEST_RESULT_DIGEST), + None, + None, + ); + let result = sign_computed_file(&mut computed_file); + assert!( + matches!( + result, + Err(ReplicateStatusCause::PostComputeTeeChallengePrivateKeyMissing) + ), + "Should return PostComputeTeeChallengePrivateKeyMissing error" + ); + }, + ); + } + + #[test] + fn sign_computed_file_returns_error_when_task_id_is_none() { + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::SignWorkerAddress.name(), + Some(TEST_WORKER_ADDRESS), + ), + ( + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey.name(), + Some(TEST_TEE_CHALLENGE_PRIVATE_KEY), + ), + ], + || { + let mut computed_file = + make_computed_file(None, None, None, Some(TEST_RESULT_DIGEST), None, None); + let result = sign_computed_file(&mut computed_file); + assert!(result.is_err(), "Should fail when task_id is None"); + }, + ); + } + + #[test] + fn sign_computed_file_returns_error_when_result_digest_is_none() { + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::SignWorkerAddress.name(), + Some(TEST_WORKER_ADDRESS), + ), + ( + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey.name(), + Some(TEST_TEE_CHALLENGE_PRIVATE_KEY), + ), + ], + || { + let mut computed_file = + make_computed_file(Some(TEST_TASK_ID), None, None, None, None, None); + let result = sign_computed_file(&mut computed_file); + assert!(result.is_err(), "Should fail when result_digest is None"); + }, + ); + } + + #[test] + fn sign_computed_file_produces_deterministic_signature() { + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::SignWorkerAddress.name(), + Some(TEST_WORKER_ADDRESS), + ), + ( + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey.name(), + Some(TEST_TEE_CHALLENGE_PRIVATE_KEY), + ), + ], + || { + let mut computed_file1 = make_computed_file( + Some(TEST_TASK_ID), + None, + None, + Some(TEST_RESULT_DIGEST), + None, + None, + ); + let result1 = sign_computed_file(&mut computed_file1); + + let mut computed_file2 = make_computed_file( + Some(TEST_TASK_ID), + None, + None, + Some(TEST_RESULT_DIGEST), + None, + None, + ); + let result2 = sign_computed_file(&mut computed_file2); + + assert!(result1.is_ok() && result2.is_ok()); + assert_eq!( + computed_file1.enclave_signature, computed_file2.enclave_signature, + "Same inputs should produce the same signature" + ); + }, + ); + } + // endregion +} diff --git a/post-compute/src/compute/dropbox.rs b/post-compute/src/compute/dropbox.rs new file mode 100644 index 0000000..13befc4 --- /dev/null +++ b/post-compute/src/compute/dropbox.rs @@ -0,0 +1,332 @@ +//! Dropbox upload service for handling file uploads to Dropbox storage. +//! +//! This module provides a small utility for uploading computation results to +//! Dropbox using the Content API "files/upload" HTTPS endpoint. It focuses on +//! correctness, explicit error mapping to `ReplicateStatusCause`, and testability +//! (the base URL is injectable for mocking). + +use crate::compute::errors::ReplicateStatusCause; +use log::{error, info}; +#[cfg(test)] +use mockall::automock; +use reqwest::blocking::Client; +use serde::Deserialize; +use std::{fs, path::Path}; + +/// Default Dropbox Content API base URL used for uploads. +pub const DROPBOX_CONTENT_BASE_URL: &str = "https://content.dropboxapi.com"; + +/// REST path for the Dropbox "files/upload" endpoint. +const FILES_UPLOAD_PATH: &str = "/2/files/upload"; + +/// Service for handling Dropbox file uploads. +/// +/// This is a lightweight utility type. Use with `DropboxService` and call +/// [`DropboxService::upload_file`]. +pub struct DropboxService; + +#[cfg_attr(test, automock)] +pub trait DropboxUploader { + fn upload_file( + &self, + access_token: &str, + local_file_path: &str, + dropbox_path: &str, + content_base_url: &str, + ) -> Result; +} + +#[derive(Deserialize, Debug)] +struct UploadResponse { + path_display: Option, +} + +impl DropboxUploader for DropboxService { + /// Uploads a file to Dropbox. + /// + /// Optimized for small to medium-sized files that can be sent in a single request. + /// For very large files (> 150 MiB), use Dropbox upload sessions (chunked upload). + /// + /// # Arguments + /// + /// - `access_token`: Dropbox API access token (Bearer token) + /// - `local_file_path`: Local path to the file to upload + /// - `dropbox_path`: Destination path in Dropbox (e.g., "/results/file.zip") + /// - `content_base_url`: Base URL for the Content API (override in tests) + /// + /// # Returns + /// + /// - `Ok(String)`: The display path of the uploaded file in Dropbox + /// - `Err(ReplicateStatusCause)`: When any step of the upload fails + /// + /// # Errors + /// + /// Returns `PostComputeResultFileNotFound` if the local file does not exist. + /// Returns `PostComputeDropboxUploadFailed` for any HTTP or API error (including 401). + /// + /// # Example + /// + /// ```rust + /// use tee_worker_post_compute::compute::dropbox::{ + /// DROPBOX_CONTENT_BASE_URL, + /// DropboxService, + /// DropboxUploader, + /// }; + /// + /// let result = DropboxService.upload_file( + /// "access-token", + /// "/tmp/file.zip", + /// "/results/file.zip", + /// DROPBOX_CONTENT_BASE_URL, + /// ); + /// // Handle result: Ok(path) | Err(cause) + /// ``` + fn upload_file( + &self, + access_token: &str, + local_file_path: &str, + dropbox_path: &str, + content_base_url: &str, + ) -> Result { + // Validate local file exists + let path = Path::new(local_file_path); + if !path.exists() { + error!("Local file not found for Dropbox upload [path:{local_file_path}]"); + return Err(ReplicateStatusCause::PostComputeResultFileNotFound); + } + + let content = fs::read(path).map_err(|e| { + error!("Failed to read file for Dropbox upload [path:{local_file_path}, error:{e}]"); + ReplicateStatusCause::PostComputeDropboxUploadFailed + })?; + + let api_arg_header = serde_json::json!({ + "autorename": false, + "mode": "add", + "mute": false, + "path": dropbox_path, + "strict_conflict": false + }) + .to_string(); + + let url = format!("{content_base_url}{FILES_UPLOAD_PATH}"); + let response = Client::new() + .post(url) + .header("Authorization", format!("Bearer {access_token}")) + .header("Content-Type", "application/octet-stream") + .header("Dropbox-API-Arg", api_arg_header) + .body(content) + .send(); + + match response { + Ok(resp) => { + let status = resp.status(); + if status.is_success() { + match resp.json::() { + Ok(meta) => { + let path = meta + .path_display + .unwrap_or_else(|| dropbox_path.to_string()); + info!("Successfully uploaded to Dropbox [path:{path}]"); + Ok(path) + } + Err(e) => { + error!("Failed to parse Dropbox response: {e}"); + Err(ReplicateStatusCause::PostComputeDropboxUploadFailed) + } + } + } else if status.as_u16() == 401 { + error!("Authentication failed - invalid or expired token"); + Err(ReplicateStatusCause::PostComputeDropboxUploadFailed) + } else { + let body = resp.text().unwrap_or_default(); + error!("Dropbox upload failed [status:{status}, body:{body}]"); + Err(ReplicateStatusCause::PostComputeDropboxUploadFailed) + } + } + Err(e) => { + error!("HTTP error calling Dropbox upload API: {e}"); + Err(ReplicateStatusCause::PostComputeDropboxUploadFailed) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + use wiremock::{ + Mock, MockServer, ResponseTemplate, + matchers::{header, method, path}, + }; + + fn create_test_computed_file() -> String { + let mut temp_file = NamedTempFile::new().unwrap(); + writeln!(temp_file, "test file content").unwrap(); + let (_file, path_buf) = temp_file.keep().unwrap(); + path_buf.to_str().unwrap().to_string() + } + + #[tokio::test] + async fn upload_returns_dropbox_path_when_server_returns_success() { + let mock_server = MockServer::start().await; + + let response_body = serde_json::json!({ + "path_display": "/results/uploaded.zip", + }); + + Mock::given(method("POST")) + .and(path(FILES_UPLOAD_PATH)) + .and(header("Authorization", "Bearer valid-token")) + .and(header("Content-Type", "application/octet-stream")) + .respond_with( + ResponseTemplate::new(200) + .insert_header("content-type", "application/json") + .set_body_json(response_body), + ) + .mount(&mock_server) + .await; + + let file_path = create_test_computed_file(); + + let base = mock_server.uri(); + let result = tokio::task::spawn_blocking(move || { + DropboxService.upload_file("valid-token", &file_path, "/results/uploaded.zip", &base) + }) + .await + .expect("The upload_file task panicked. Expected the DropboxService to successfully upload the file and return the Dropbox path, but the task did not complete as expected."); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "/results/uploaded.zip"); + + let requests = mock_server.received_requests().await.unwrap(); + assert!(!requests.is_empty()); + + let expected_args = serde_json::json!({ + "mode": "add", + "autorename": false, + "mute": false, + "path": "/results/uploaded.zip", + "strict_conflict": false + }) + .to_string(); + let arg_header = requests[0] + .headers + .get("Dropbox-API-Arg") + .map(|v| v.to_str().unwrap_or("")) + .unwrap_or(""); + assert_eq!(arg_header, expected_args); + } + + #[test] + fn upload_file_returns_error_when_local_file_not_found() { + let service = DropboxService; + let result = service.upload_file( + "fake-token", + "/non/existent/file.zip", + "/results/test.zip", + DROPBOX_CONTENT_BASE_URL, + ); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeResultFileNotFound) + ); + } + + #[tokio::test] + async fn upload_returns_error_when_server_returns_unauthorized() { + let mock_server = MockServer::start().await; + + let error_response = serde_json::json!({ + "error_summary": "invalid_access_token", + "error": { ".tag": "invalid_access_token" } + }); + + Mock::given(method("POST")) + .and(path(FILES_UPLOAD_PATH)) + .and(header("Authorization", "Bearer invalid-token")) + .respond_with( + ResponseTemplate::new(401) + .insert_header("content-type", "application/json") + .set_body_json(error_response), + ) + .mount(&mock_server) + .await; + + let file_path = create_test_computed_file(); + + let base = mock_server.uri(); + let result = tokio::task::spawn_blocking(move || { + DropboxService.upload_file("invalid-token", &file_path, "/results/uploaded.zip", &base) + }) + .await + .expect("Task panicked: expected DropboxService.upload_file to return an error indicating unauthorized access (PostComputeDropboxUploadFailed), but the task did not complete successfully"); + + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeDropboxUploadFailed) + ); + } + + #[tokio::test] + async fn upload_returns_error_when_server_returns_500() { + let mock_server = MockServer::start().await; + + Mock::given(method("POST")) + .and(path(FILES_UPLOAD_PATH)) + .and(header("Authorization", "Bearer token")) + .respond_with( + ResponseTemplate::new(500) + .insert_header("content-type", "application/json") + .set_body_string("Internal Server Error"), + ) + .mount(&mock_server) + .await; + + let file_path = create_test_computed_file(); + + let base = mock_server.uri(); + let result = tokio::task::spawn_blocking(move || { + DropboxService.upload_file("token", &file_path, "/results/uploaded.zip", &base) + }) + .await + .expect("Task panicked: expected DropboxService.upload_file to return an error indicating a server error (PostComputeDropboxUploadFailed), but the task did not complete successfully"); + + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeDropboxUploadFailed) + ); + } + + #[tokio::test] + async fn upload_returns_error_when_response_json_is_invalid() { + let mock_server = MockServer::start().await; + + Mock::given(method("POST")) + .and(path(FILES_UPLOAD_PATH)) + .and(header("Authorization", "Bearer token")) + .respond_with( + ResponseTemplate::new(200) + .insert_header("content-type", "application/json") + .set_body_string("not-json"), + ) + .mount(&mock_server) + .await; + + let file_path = create_test_computed_file(); + + let base = mock_server.uri(); + let result = tokio::task::spawn_blocking(move || { + DropboxService.upload_file("token", &file_path, "/results/bad.json", &base) + }) + .await + .expect("Task panicked: expected upload_file to return an error due to invalid JSON response, but the task did not complete successfully"); + + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeDropboxUploadFailed) + ); + } +} diff --git a/post-compute/src/compute/encryption.rs b/post-compute/src/compute/encryption.rs new file mode 100644 index 0000000..85c4a9a --- /dev/null +++ b/post-compute/src/compute/encryption.rs @@ -0,0 +1,947 @@ +use crate::compute::errors::ReplicateStatusCause; +use crate::compute::web2_result::{Web2ResultInterface, Web2ResultService}; +use aes::{ + Aes256, + cipher::{BlockEncryptMut, KeyIvInit, block_padding::Pkcs7}, +}; +use cbc::Encryptor; +use log::error; +use rand::{RngCore, rngs::OsRng}; +use rsa::{Pkcs1v15Encrypt, RsaPublicKey, pkcs8::DecodePublicKey}; +use sha3::{Digest, Sha3_256}; +use std::{fs, path::Path}; + +/// 256-bit key (32 bytes) +const AES_KEY_LENGTH: usize = 32; +/// 128-bit IV (16 bytes) same as the AES block size +const AES_IV_LENGTH: usize = 16; + +/// Encrypts a data file using hybrid encryption (AES-256-CBC + RSA-2048). +/// +/// This function implements a secure hybrid encryption scheme where the input data +/// is encrypted with AES-256-CBC and the AES key is encrypted with RSA-2048. +/// The function creates an output directory containing the encrypted data and +/// encrypted key, with optional ZIP compression. +/// +/// # Encryption Process +/// +/// 1. **File Validation**: Validates input file path and reads data +/// 2. **AES Encryption**: Generates a random 256-bit AES key and encrypts the data +/// 3. **RSA Key Encryption**: Encrypts the AES key using the provided RSA public key +/// 4. **Output Generation**: Creates encrypted files in a structured directory +/// 5. **Optional Compression**: Creates a ZIP archive if requested +/// +/// # Arguments +/// +/// * `in_data_file_path` - Path to the input file to encrypt. Must be a valid, readable, non-empty file. +/// * `plain_text_rsa_pub` - RSA public key in PEM format with proper headers/footers. +/// Must be a valid PKCS#8 or PKCS#1 formatted PEM key. +/// * `produce_zip` - If `true`, creates a ZIP archive containing encrypted files. +/// If `false`, returns the directory path containing encrypted files. +/// +/// # Returns +/// +/// * `Result` - On success, returns the path to either: +/// - ZIP file path (if `produce_zip` is `true`) +/// - Directory path containing encrypted files (if `produce_zip` is `false`) +/// +/// # Output Structure +/// +/// When `produce_zip` is `false`, creates a directory named `encrypted-{filename_stem}`: +/// ```text +/// encrypted-myfile/ +/// ├── myfile.txt.aes # AES-encrypted data (IV + ciphertext) +/// └── aes-key.rsa # RSA-encrypted AES key (raw bytes) +/// ``` +/// +/// When `produce_zip` is `true`, creates `iexec_out.zip` containing the above structure. +/// +/// # Errors +/// +/// * `PostComputeEncryptionFailed` - Returned for any failure including: +/// - Invalid file path or unreadable input file +/// - Empty input files +/// - Invalid or malformed RSA public keys +/// - Cryptographic operation failures +/// - File system operation failures (directory creation, file writing) +/// - ZIP creation failures +/// +/// # Security Notes +/// +/// - Each encryption operation uses a fresh AES key and IV +/// - RSA encryption uses PKCS#1 v1.5 padding (industry standard) +/// - All random values are generated using cryptographically secure `OsRng` +/// - Input data is securely overwritten in memory after encryption +/// - AES keys are stored as raw encrypted bytes (not Base64 encoded) +/// +/// # Example +/// +/// ```rust +/// use tee_worker_post_compute::compute::encryption::encrypt_data; +/// +/// const RSA_PUBLIC_KEY: &str = r#"-----BEGIN PUBLIC KEY----- +/// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0mx20CSFczJaM4rtYfL +/// VHXfTybD4J85SGrI6GfPlOhAnocZOMIRJVqrYSGqfNvw6bnv3OrNp0OJ6Av7v20r +/// YiciyJ/R9c7W4jLksTC0qAEr1x8IsH1rsTcgIhD+V2eQWqi05ArUg+YDQiGr/B6T +/// jJRbbZIjcX6l/let03NJ8b6vMgaY+6tpt9GXhm27/tVIG6vt0NYViU0cOY3+fRH7 +/// M1XvGQa3D0LnJTvhAgljz3Jpl7whAWQgluVDVNq7erJVN7/d5jpTG29FWrAYujvs +/// KfizbB8KpGwCHwFcHZurz9+Sp4mH5cQCvz/VhFrAvzbhsIl6Qf8XURHmqxYc/DRt +/// FQIDAQAB +/// -----END PUBLIC KEY-----"#; +/// +/// let temp_file = tempfile::NamedTempFile::new().expect("Failed to create temp file"); +/// match std::fs::write(temp_file.path(), b"Data to encrypt") { +/// Ok(_) => println!("Successfully wrote to temp file"), +/// Err(e) => eprintln!("Failed to write to temp file: {}", e), +/// } +/// let file = match temp_file.path().to_str() { +/// Some(f) => f, +/// _ => "error_converting_temp_file_path_to_string", +/// }; +/// +/// // Encrypt a file and create a ZIP archive +/// match encrypt_data(file, RSA_PUBLIC_KEY, true) { +/// Ok(result) => println!("Encrypted ZIP created: {}", result), +/// Err(e) => eprintln!("Failed to encrypt file and create ZIP archive: {:?}", e), +/// } +/// +/// // Encrypt a file and do not create a ZIP archive +/// match encrypt_data(file, RSA_PUBLIC_KEY, false) { +/// Ok(result) => println!("Encrypted files in: {}", result), +/// Err(e) => eprintln!("Failed to encrypt file and create directory: {:?}", e), +/// } +/// ``` +pub fn encrypt_data( + in_data_file_path: &str, + plain_text_rsa_pub: &str, + produce_zip: bool, +) -> Result { + let path = Path::new(in_data_file_path); + let in_data_filename = path + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| { + error!("Failed to extract filename from path: {in_data_file_path}"); + ReplicateStatusCause::PostComputeEncryptionFailed + })?; + let out_encrypted_data_filename = format!("{in_data_filename}.aes"); + + let work_dir = path.parent().and_then(|p| p.to_str()).ok_or_else(|| { + error!("Failed to get parent directory of: {in_data_file_path}"); + ReplicateStatusCause::PostComputeEncryptionFailed + })?; + + let filename_without_ext = + path.file_stem() + .and_then(|stem| stem.to_str()) + .ok_or_else(|| { + error!("Failed to extract filename without extension from '{in_data_file_path}'"); + ReplicateStatusCause::PostComputeEncryptionFailed + })?; + let out_enc_dir = format!("{work_dir}/encrypted-{filename_without_ext}"); //location of future encrypted files (./encrypted-0x1_result) + + // Get data to encrypt + let data = fs::read(in_data_file_path).map_err(|e| { + error!( + "Failed to encrypt_data (read_file error) [in_data_file_path:{in_data_file_path}]: {e}" + ); + ReplicateStatusCause::PostComputeEncryptionFailed + })?; + if data.is_empty() { + error!("Failed to encrypt_data (empty file error) [in_data_file_path:{in_data_file_path}]"); + return Err(ReplicateStatusCause::PostComputeEncryptionFailed); + } + + // Generate AES key for data encryption + let aes_key = generate_aes_key().map_err(|_| { + error!("Failed to encrypt_data (generate_aes_key error) [in_data_file_path:{in_data_file_path}]"); + ReplicateStatusCause::PostComputeEncryptionFailed + })?; + + // Encrypt data with Base64 AES key + let encrypted_data = aes_encrypt(&data, &aes_key).map_err(|e| { + error!("Failed to encrypt_data (aes_encrypt error) [in_data_file_path:{in_data_file_path}]: {e}"); + ReplicateStatusCause::PostComputeEncryptionFailed + })?; + + // Create folder for future out_encrypted_data & out_encrypted_aes_key + let out_enc_dir_path = std::path::Path::new(&out_enc_dir); + if !out_enc_dir_path.exists() { + fs::create_dir_all(out_enc_dir_path).map_err(|e| { + error!("Failed to create directory '{out_enc_dir}' (is_out_dir_created error) [in_data_file_path:{in_data_file_path}]: {e}"); + ReplicateStatusCause::PostComputeEncryptionFailed + })?; + } + + // Store encrypted data in ./0xtask1 [out_enc_dir] + write_file( + format!("{out_enc_dir}/{out_encrypted_data_filename}"), + &encrypted_data, + ) + .map_err(|_| { + error!("Failed to encrypt_data (is_encrypted_data_stored error) [in_data_file_path:{in_data_file_path}]"); + ReplicateStatusCause::PostComputeEncryptionFailed + })?; + + // Encrypt AES key with RSA public key + let encrypted_aes_key = RsaPublicKey::from_public_key_pem(plain_text_rsa_pub) + .map_err(|e| { + error!("Failed to parse RSA public key: {e}"); + ReplicateStatusCause::PostComputeEncryptionFailed + })? + .encrypt(&mut OsRng, Pkcs1v15Encrypt, &aes_key) + .map_err(|e| { + error!("RSA encryption failed: {e}"); + ReplicateStatusCause::PostComputeEncryptionFailed + })?; + + // Store encrypted AES key in ./0xtask1 [outEncDir] + write_file( + format!("{out_enc_dir}/aes-key.rsa"), + &encrypted_aes_key, + ) + .map_err(|_| { + error!("Failed to encrypt_data (is_encrypted_aes_key_stored error) [in_data_file_path:{in_data_file_path}]"); + ReplicateStatusCause::PostComputeEncryptionFailed + })?; + + if produce_zip { + // Zip encrypted files folder + let parent = out_enc_dir_path.parent().unwrap_or_else(|| Path::new(".")); + let out_enc_zip = Web2ResultService + .zip_iexec_out(&out_enc_dir, parent.to_str().unwrap()) + .map_err(|_| { + error!("Failed to encrypt_data (out_enc_zip error) [in_data_file_path:{in_data_file_path}]"); + ReplicateStatusCause::PostComputeEncryptionFailed + })?; + if out_enc_zip.is_empty() { + error!( + "Failed to encrypt_data (out_enc_zip error) [in_data_file_path:{in_data_file_path}]" + ); + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + } else { + Ok(out_enc_zip) + } + } else { + Ok(out_enc_dir) + } +} + +/// Generates a cryptographically secure 256-bit AES key. +/// +/// This function creates a new AES-256 key using the operating system's +/// cryptographically secure random number generator (`OsRng`). Each call +/// produces a unique key suitable for encrypting sensitive data. +/// +/// # Returns +/// +/// * `Result, ReplicateStatusCause>` - On success, returns `AES_KEY_LENGTH` bytes +/// vector containing the AES-256 key. On failure, returns `PostComputeEncryptionFailed`. +/// +/// # Security +/// +/// - Uses `OsRng` which provides cryptographically secure randomness +/// - Generates full 256-bit keys for maximum security +/// - Each key is statistically unique across all invocations +/// +/// # Errors +/// +/// * `PostComputeEncryptionFailed` - If the random number generator fails +/// to produce sufficient entropy (extremely rare on modern systems) +/// +/// # Note +/// +/// This is an internal helper method used by the public [`encrypt_data`] function. +pub fn generate_aes_key() -> Result, ReplicateStatusCause> { + let mut key_bytes = [0u8; AES_KEY_LENGTH]; + if let Err(e) = OsRng.try_fill_bytes(&mut key_bytes) { + error!("Failed to generate AES key: {e}"); + return Err(ReplicateStatusCause::PostComputeEncryptionFailed); + } + Ok(key_bytes.to_vec()) +} + +/// Encrypts data using AES-256 in CBC mode with PKCS#7 padding. +/// +/// This function implements AES-256-CBC encryption with the following characteristics: +/// - **Algorithm**: AES-256 (Advanced Encryption Standard with 256-bit key) +/// - **Mode**: CBC (Cipher Block Chaining) for semantic security +/// - **Padding**: PKCS#7 to handle arbitrary input lengths +/// - **IV**: Random 128-bit initialization vector prepended to output +/// +/// # Process +/// +/// 1. Validates input data and key length +/// 2. Generates a random 128-bit IV using `OsRng` +/// 3. Encrypts data using AES-256-CBC with PKCS#7 padding +/// 4. Prepends IV to ciphertext for later decryption +/// +/// # Arguments +/// +/// * `data` - The plaintext data to encrypt. Must not be empty. +/// * `key` - The AES-256 key, whose length will be validated. +/// +/// # Returns +/// +/// * `Result, ReplicateStatusCause>` - On success, returns a vector +/// containing `[IV][Ciphertext]` where: +/// - First `AES_IV_LENGTH` bytes: Random initialization vector +/// - Remaining bytes: AES-encrypted data with PKCS#7 padding +/// +/// # Output Format +/// +/// ```text +/// [IV: `AES_IV_LENGTH` bytes][Encrypted Data: variable length, multiple of block size (16 bytes)] +/// ``` +/// +/// # Security Properties +/// +/// - **Semantic Security**: CBC mode with random IV ensures identical plaintexts +/// produce different ciphertexts +/// - **Integrity**: While this function doesn't provide authentication, +/// the padding scheme prevents certain classes of attacks +/// - **Key Schedule**: AES-256 uses 14 rounds for maximum security +/// +/// # Errors +/// +/// * `PostComputeEncryptionFailed` - If: +/// - Input data is empty +/// - Key is not exactly `AES_KEY_LENGTH` bytes +/// - Random number generation fails +/// - Encryption operation fails +/// +/// # Note +/// +/// This is an internal helper method used by the public [`encrypt_data`] function. +pub fn aes_encrypt(data: &[u8], key: &[u8]) -> Result, ReplicateStatusCause> { + if data.is_empty() { + error!("AES encryption input data is empty"); + return Err(ReplicateStatusCause::PostComputeEncryptionFailed); + } + if key.len() != AES_KEY_LENGTH { + error!( + "AES encryption key must be {AES_KEY_LENGTH} bytes, got {}", + key.len() + ); + return Err(ReplicateStatusCause::PostComputeEncryptionFailed); + } + + // Generate random `AES_IV_LENGTH` bytes initialization vector + let mut iv = [0u8; AES_IV_LENGTH]; + if let Err(e) = OsRng.try_fill_bytes(&mut iv) { + error!("Failed to generate IV for AES encryption: {e}"); + return Err(ReplicateStatusCause::PostComputeEncryptionFailed); + } + + // Perform AES-256-CBC encryption with PKCS#7 padding + let cipher = Encryptor::::new(key.into(), &iv.into()); + let ciphertext = cipher.encrypt_padded_vec_mut::(data); + + // Prepend IV to ciphertext + let mut result = Vec::with_capacity(iv.len() + ciphertext.len()); + result.extend_from_slice(&iv); + result.extend_from_slice(&ciphertext); + + Ok(result) +} + +/// Writes data to a file with secure error handling and logging. +/// +/// This function writes binary data to the specified file path, creating +/// the file if it doesn't exist or overwriting it if it does. The function +/// includes comprehensive error logging with data integrity hashing for +/// debugging purposes. +/// +/// # Arguments +/// +/// * `file_path` - The target file path as a String. Must be writable. +/// * `data` - The binary data to write to the file. +/// +/// # Returns +/// +/// * `Result<(), ReplicateStatusCause>` - Success if file was written, +/// error if write operation failed. +/// +/// # Error Handling +/// +/// On write failure, the function: +/// 1. Computes SHA3-256 hash of the data (for debugging, not security) +/// 2. Logs error with file path and data hash (no sensitive data exposed) +/// 3. Returns `PostComputeEncryptionFailed` +/// +/// # Errors +/// +/// * `PostComputeEncryptionFailed` - If write fails due to: +/// - Insufficient disk space +/// - Permission denied +/// - Invalid file path +/// - Filesystem errors +/// +/// # Note +/// +/// This is an internal helper method used by the public [`encrypt_data`] function. +pub fn write_file(file_path: String, data: &[u8]) -> Result<(), ReplicateStatusCause> { + if let Err(e) = fs::write(&file_path, data) { + let mut hasher = Sha3_256::new(); + hasher.update(data); + let hash = hasher.finalize(); + let hash_hex = format!("{hash:x}"); + error!("Failed to write file [file_path:{file_path}, data_hash:{hash_hex}]: {e}"); + return Err(ReplicateStatusCause::PostComputeEncryptionFailed); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{fs::File, io::Read}; + use tempfile::tempdir; + use zip::ZipArchive; + + const TEST_RSA_PUBLIC_KEY_PEM: &str = r#"-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0mx20CSFczJaM4rtYfL +VHXfTybD4J85SGrI6GfPlOhAnocZOMIRJVqrYSGqfNvw6bnv3OrNp0OJ6Av7v20r +YiciyJ/R9c7W4jLksTC0qAEr1x8IsH1rsTcgIhD+V2eQWqi05ArUg+YDQiGr/B6T +jJRbbZIjcX6l/let03NJ8b6vMgaY+6tpt9GXhm27/tVIG6vt0NYViU0cOY3+fRH7 +M1XvGQa3D0LnJTvhAgljz3Jpl7whAWQgluVDVNq7erJVN7/d5jpTG29FWrAYujvs +KfizbB8KpGwCHwFcHZurz9+Sp4mH5cQCvz/VhFrAvzbhsIl6Qf8XURHmqxYc/DRt +FQIDAQAB +-----END PUBLIC KEY-----"#; + + // region encrypt_data + #[test] + fn encrypt_data_produces_directory_when_valid_input_and_produce_zip_false() { + let base_temp = tempdir().expect("Failed to create base temp dir for encrypt_data test"); + let input_dir = base_temp.path().join("input_data_dir_nozip"); + fs::create_dir_all(&input_dir).expect("Failed to create input_data_dir_nozip"); + + let input_file_path = input_dir.join("another_result.dat"); + let original_data = b"Data for no-zip scenario."; + fs::write(&input_file_path, original_data) + .expect("Failed to write to temporary input file"); + + let result = encrypt_data( + input_file_path.to_str().unwrap(), + TEST_RSA_PUBLIC_KEY_PEM, + false, // produce_zip = false + ); + assert!( + result.is_ok(), + "encrypt_data should succeed. Error: {:?}", + result.err() + ); + + let output_dir_path_str = result.unwrap(); + let output_dir_path = Path::new(&output_dir_path_str); + assert!( + output_dir_path.exists(), + "Output directory should exist at {output_dir_path_str}" + ); + assert!( + output_dir_path.is_dir(), + "Output path should be a directory." + ); + + let expected_dir_name = format!( + "encrypted-{}", + input_file_path.file_stem().unwrap().to_str().unwrap() + ); + assert_eq!( + output_dir_path.file_name().unwrap().to_str().unwrap(), + expected_dir_name, + "Output directory has unexpected name." + ); + assert_eq!( + output_dir_path.parent().unwrap(), + input_dir, + "Output directory created in unexpected parent." + ); + + let expected_encrypted_data_filename = format!( + "{}.aes", + input_file_path.file_name().unwrap().to_str().unwrap() + ); + let encrypted_file_in_dir = output_dir_path.join(expected_encrypted_data_filename); + assert!( + encrypted_file_in_dir.exists(), + "Encrypted data file not found in output directory." + ); + + let encrypted_content = + fs::read(&encrypted_file_in_dir).expect("Failed to read encrypted file from dir"); + assert!(!encrypted_content.is_empty()); + assert_ne!(encrypted_content, original_data); + + let aes_key_file_in_dir = output_dir_path.join("aes-key.rsa"); + assert!( + aes_key_file_in_dir.exists(), + "AES key file not found in output directory." + ); + + let aes_key_content_bytes = + fs::read(&aes_key_file_in_dir).expect("Failed to read AES key file from dir"); + assert!(!aes_key_content_bytes.is_empty()); + // AES key is now stored as raw encrypted bytes, not Base64 string + assert_eq!(aes_key_content_bytes.len(), 256); // RSA-2048 produces 256 bytes output + } + + #[test] + fn encrypt_data_produces_zip_file_when_valid_input_and_produce_zip_true() { + let base_temp = tempdir().expect("Failed to create base temp dir for encrypt_data test"); + let input_dir = base_temp.path().join("input_data_dir"); + fs::create_dir_all(&input_dir).expect("Failed to create input_data_dir"); + + let input_file_path = input_dir.join("my_result_data.txt"); + let original_data = b"This is the secret data to be encrypted and zipped!"; + fs::write(&input_file_path, original_data) + .expect("Failed to write to temporary input file"); + + let result = encrypt_data( + input_file_path.to_str().unwrap(), + TEST_RSA_PUBLIC_KEY_PEM, + true, // produce_zip = true + ); + assert!( + result.is_ok(), + "encrypt_data should succeed. Error: {:?}", + result.err() + ); + + let output_zip_path_str = result.unwrap(); + let output_zip_path = Path::new(&output_zip_path_str); + assert!( + output_zip_path.exists(), + "Output zip file should exist at {output_zip_path_str}" + ); + assert_eq!( + output_zip_path.extension().unwrap_or_default(), + "zip", + "Output file should be a zip file" + ); + + let expected_zip_parent = input_dir; // work_dir + let expected_zip_name_stem = "iexec_out"; + assert_eq!( + output_zip_path.parent().unwrap(), + expected_zip_parent, + "Zip file created in unexpected parent directory." + ); + assert_eq!( + output_zip_path.file_stem().unwrap().to_str().unwrap(), + expected_zip_name_stem, + "Zip file has unexpected stem." + ); + + let zip_file_reader = File::open(output_zip_path).expect("Failed to open output zip file"); + let mut archive = + ZipArchive::new(zip_file_reader).expect("Failed to read output zip archive"); + assert!( + archive.len() == 2, + "Zip archive should contain 2 files. Found: {}", + archive.len() + ); + + let expected_encrypted_data_filename = format!( + "{}.aes", + input_file_path.file_name().unwrap().to_str().unwrap() + ); + + // Check for encrypted data file + { + let encrypted_data_entry = archive.by_name(&expected_encrypted_data_filename); + assert!( + encrypted_data_entry.is_ok(), + "Encrypted data file '{expected_encrypted_data_filename}' not found in zip." + ); + let mut encrypted_data_file_in_zip = encrypted_data_entry.unwrap(); + let mut encrypted_content = Vec::new(); + encrypted_data_file_in_zip + .read_to_end(&mut encrypted_content) + .expect("Failed to read encrypted data from zip"); + assert!( + !encrypted_content.is_empty(), + "Encrypted data in zip should not be empty." + ); + assert_ne!( + encrypted_content, original_data, + "Encrypted data in zip should not match original data." + ); + } + + // Check for encrypted AES key file + { + let aes_key_entry = archive.by_name("aes-key.rsa"); + assert!( + aes_key_entry.is_ok(), + "AES key file 'aes-key.rsa' not found in zip." + ); + let mut aes_key_file_in_zip = aes_key_entry.unwrap(); + let mut aes_key_content_bytes = Vec::new(); + aes_key_file_in_zip + .read_to_end(&mut aes_key_content_bytes) + .expect("Failed to read AES key from zip"); + assert!( + !aes_key_content_bytes.is_empty(), + "AES key content in zip should not be empty." + ); + // AES key is now stored as raw encrypted bytes, not Base64 string + assert_eq!( + aes_key_content_bytes.len(), + 256, + "RSA-2048 produces 256 bytes output" + ); + } + } + + #[test] + fn encrypt_data_returns_error_when_input_file_is_empty() { + let base_temp = tempdir().expect("Failed to create base temp dir"); + let input_dir = base_temp.path().join("input_empty_file_dir"); + fs::create_dir_all(&input_dir).expect("Failed to create dir for empty file test"); + let empty_file_path = input_dir.join("empty_data.txt"); + File::create(&empty_file_path).expect("Failed to create empty file"); // Create 0 byte file + + let result = encrypt_data( + empty_file_path.to_str().unwrap(), + TEST_RSA_PUBLIC_KEY_PEM, + true, // produce_zip doesn't matter here + ); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + } + + #[test] + fn encrypt_data_returns_error_when_rsa_key_is_invalid() { + // Edge case: invalid RSA key, should now return Err instead of Ok("") + let base_temp = tempdir().expect("Failed to create base temp dir"); + let input_dir = base_temp.path().join("input_invalid_key_dir"); + fs::create_dir_all(&input_dir).expect("Failed to create dir for invalid key test"); + let input_file_path = input_dir.join("some_data.bin"); + fs::write(&input_file_path, b"some valid data content") + .expect("Failed to write input file for invalid key test"); + let invalid_rsa_key = "THIS IS NOT A VALID RSA KEY"; + + let result = encrypt_data( + input_file_path.to_str().unwrap(), + invalid_rsa_key, + true, // produce_zip doesn't matter + ); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + } + + #[test] + fn encrypt_data_returns_error_when_input_file_path_is_invalid() { + let base_temp = tempdir().expect("Failed to create base temp dir"); + let non_existent_file_path = base_temp.path().join("non_existent_input.txt"); + + let result = encrypt_data( + non_existent_file_path.to_str().unwrap(), + TEST_RSA_PUBLIC_KEY_PEM, + true, + ); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + } + + #[test] + fn encrypt_data_produces_encrypted_output_when_input_is_binary() { + let base_temp = tempdir().expect("Failed to create base temp dir for binary encrypt test"); + let input_dir = base_temp.path().join("input_binary_encrypt"); + fs::create_dir_all(&input_dir).unwrap(); + let input_file_path = input_dir.join("data_to_encrypt.bin"); + let binary_content = vec![0, 159, 146, 150, 255, 0, 100, 200, 50, 10, 0, 255]; + fs::write(&input_file_path, &binary_content).expect("Failed to write binary data"); + + let result = encrypt_data( + input_file_path.to_str().unwrap(), + TEST_RSA_PUBLIC_KEY_PEM, + true, // produce_zip = true + ); + assert!( + result.is_ok(), + "encrypt_data should succeed for binary input. Error: {:?}", + result.err() + ); + + let output_zip_path_str = result.unwrap(); + let output_zip_path = Path::new(&output_zip_path_str); + assert!( + output_zip_path.exists(), + "Encrypted zip file should exist at {output_zip_path_str}" + ); + assert_eq!(output_zip_path.extension().unwrap_or_default(), "zip"); + } + + #[test] + fn encrypt_data_returns_error_when_output_dir_creation_fails() { + let temp_dir = tempfile::tempdir().unwrap(); + let input_file_path = temp_dir.path().join("input.txt"); + fs::write(&input_file_path, b"data").unwrap(); + + let output_dir_path = temp_dir.path().join("encrypted-input"); + fs::write(&output_dir_path, b"not a dir").unwrap(); + + let result = encrypt_data( + input_file_path.to_str().unwrap(), + TEST_RSA_PUBLIC_KEY_PEM, + false, + ); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + } + + #[test] + fn encrypt_data_returns_error_when_zip_fails_due_to_unwritable_destination() { + let temp_dir = tempfile::tempdir().unwrap(); + let input_file_path = temp_dir.path().join("input.txt"); + fs::write(&input_file_path, b"data").unwrap(); + + let zip_file_path = temp_dir.path().join("iexec_out.zip"); + fs::create_dir(&zip_file_path).unwrap(); + + let result = encrypt_data( + input_file_path.to_str().unwrap(), + TEST_RSA_PUBLIC_KEY_PEM, + true, + ); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + } + // endregion + + // region generate_aes_key + #[test] + fn generate_aes_key_returns_32_bytes_when_successful() { + let result = generate_aes_key(); + assert!(result.is_ok()); + + let key = result.unwrap(); + assert_eq!(key.len(), AES_KEY_LENGTH); + } + + #[test] + fn generate_aes_key_returns_different_keys_when_called_multiple_times() { + let key1 = generate_aes_key().unwrap(); + let key2 = generate_aes_key().unwrap(); + assert_ne!(key1, key2); + } + // endregion + + // region aes_encrypt + #[test] + fn aes_encrypt_returns_encrypted_data_with_correct_length_and_padding_when_input_is_valid() { + let data = b"This is some test data."; + let key = generate_aes_key().expect("Failed to generate AES key for test"); + + let encrypted_result = aes_encrypt(data, &key); + assert!(encrypted_result.is_ok()); + + let encrypted_data = encrypted_result.unwrap(); + assert_ne!(data, encrypted_data.as_slice()); + + const AES_BLOCK_SIZE: usize = 16; // AES block size (16 bytes) + // AES_CBC_PKCS7: output is IV (`AES_IV_LENGTH` bytes) + ciphertext (multiple of block size, AES_BLOCK_SIZE bytes) + // PKCS7 padding: adds (AES_BLOCK_SIZE - (data.len() % AES_BLOCK_SIZE)) bytes + // If data.len() % AES_BLOCK_SIZE == 0, adds a full AES_BLOCK_SIZE bytes of padding + // More precisely, IV_SIZE + DATA_SIZE + PADDING_SIZE + let padding_needed = AES_BLOCK_SIZE - (data.len() % AES_BLOCK_SIZE); + let expected_exact_len = AES_IV_LENGTH + data.len() + padding_needed; + assert_eq!( + encrypted_data.len(), + expected_exact_len, + "Encrypted data length is unexpected. Got {}, expected {}. Original data length: {}", + encrypted_data.len(), + expected_exact_len, + data.len() + ); + assert!( + encrypted_data.len() > data.len(), + "Encrypted data should be longer than original data due to IV and padding." + ); + + let iv = &encrypted_data[0..AES_IV_LENGTH]; + assert_ne!(iv, &[0u8; AES_IV_LENGTH], "IV should not be all zeros"); + } + + #[test] + fn aes_encrypt_returns_error_when_data_is_empty() { + let data = b""; + let key = generate_aes_key().expect("Failed to generate AES key for test"); + + let encrypted_result = aes_encrypt(data, &key); + assert!(encrypted_result.is_err()); + assert_eq!( + encrypted_result, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + } + + #[test] + fn aes_encrypt_returns_error_when_key_wrong_size() { + let data = b"test data"; + let wrong_key = vec![0u8; 16]; // 16 bytes instead of 32 + + let result = aes_encrypt(data, &wrong_key); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + } + + #[test] + fn aes_encrypt_returns_different_results_when_called_multiple_times() { + let data = b"test data"; + let key = generate_aes_key().unwrap(); + + let encrypted1 = aes_encrypt(data, &key).unwrap(); + let encrypted2 = aes_encrypt(data, &key).unwrap(); + assert_ne!(encrypted1, encrypted2); + } + + #[test] + fn aes_encrypt_returns_error_when_key_is_invalid_length() { + let data = b"Some data"; + let short_key = b"shortkey"; // Not 32 bytes + let long_key = b"thisisaverylongkeythatisdefinitelymorethan32bytes"; // Not 32 bytes + + let encrypted_result_short = aes_encrypt(data, short_key); + assert!(encrypted_result_short.is_err()); + assert_eq!( + encrypted_result_short, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + + let encrypted_result_long = aes_encrypt(data, long_key); + assert!(encrypted_result_long.is_err()); + assert_eq!( + encrypted_result_long, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + } + // endregion + + // region write_file + #[test] + fn write_file_creates_file_when_successful() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("test.txt"); + let data = b"test content"; + + let result = write_file(file_path.to_str().unwrap().to_string(), data); + assert!(result.is_ok()); + assert!(file_path.exists()); + + let content = fs::read(&file_path).unwrap(); + assert_eq!(content, data); + } + + #[test] + fn write_file_returns_error_when_invalid_path() { + let invalid_path = "/invalid/path/that/does/not/exist/file.txt"; + let data = b"test content"; + + let result = write_file(invalid_path.to_string(), data); + assert!(result.is_err()); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + } + + #[test] + fn write_file_overwrites_existing_file_when_called_twice() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("test.txt"); + let data1 = b"first content"; + let data2 = b"second content"; + + let result1 = write_file(file_path.to_str().unwrap().to_string(), data1); + let result2 = write_file(file_path.to_str().unwrap().to_string(), data2); + assert!(result1.is_ok()); + assert!(result2.is_ok()); + + let content = fs::read(&file_path).unwrap(); + assert_eq!(content, data2); + } + + #[test] + fn write_file_handles_empty_data_when_called() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("empty.txt"); + let data = b""; + + let result = write_file(file_path.to_str().unwrap().to_string(), data); + assert!(result.is_ok()); + assert!(file_path.exists()); + + let content = fs::read(&file_path).unwrap(); + assert_eq!(content, data); + } + + #[test] + fn write_file_writes_data_to_file_when_path_is_valid() { + let base_dir = tempdir().expect("Failed to create temp base directory for test"); + let file_path = base_dir.path().join("test_output.txt"); + let data_to_write = b"Hello, Jules! This is a test."; + + let result = write_file(file_path.to_str().unwrap().to_string(), data_to_write); + assert!( + result.is_ok(), + "write_file should succeed. Error: {:?}", + result.err() + ); + assert!(file_path.exists(), "File should exist after writing."); + + let mut file_content = Vec::new(); + let mut file = + fs::File::open(&file_path).expect("Failed to open written file for verification"); + file.read_to_end(&mut file_content) + .expect("Failed to read written file for verification"); + assert_eq!( + file_content, data_to_write, + "File content does not match written data." + ); + } + + #[test] + fn write_file_returns_error_when_path_is_invalid() { + let invalid_path = "/nonexistent_directory_test/test_output.txt"; // Assuming this path is not writable + let base_dir = tempdir().expect("Failed to create temp base directory for test"); + let dir_as_file_path = base_dir.path().join("i_am_a_directory"); + fs::create_dir_all(&dir_as_file_path) + .expect("Failed to create directory for collision test"); + let data_to_write = b"Some data"; + + let result_nonexistent_parent = write_file(invalid_path.to_string(), data_to_write); + if result_nonexistent_parent.is_ok() { + let _ = fs::remove_file(invalid_path); + let _ = fs::remove_dir(Path::new(invalid_path).parent().unwrap()); + } + assert_eq!( + result_nonexistent_parent, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + + let result_path_is_dir = write_file( + dir_as_file_path.to_str().unwrap().to_string(), + data_to_write, + ); + assert_eq!( + result_path_is_dir, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + } + // endregion +} diff --git a/post-compute/src/compute/errors.rs b/post-compute/src/compute/errors.rs new file mode 100644 index 0000000..36978aa --- /dev/null +++ b/post-compute/src/compute/errors.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, PartialEq, Clone, Error, Serialize, Deserialize)] +#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))] +#[allow(clippy::enum_variant_names)] +pub enum ReplicateStatusCause { + #[error("computed.json file missing")] + PostComputeComputedFileNotFound, + #[error("Failed to upload to Dropbox")] + PostComputeDropboxUploadFailed, + #[error("Encryption stage failed")] + PostComputeEncryptionFailed, + #[error("Encryption public key related environment variable is missing")] + PostComputeEncryptionPublicKeyMissing, + #[error("Unexpected error occurred")] + PostComputeFailedUnknownIssue, + #[error("Invalid enclave challenge private key")] + PostComputeInvalidEnclaveChallengePrivateKey, + #[error("Invalid TEE signature")] + PostComputeInvalidTeeSignature, + #[error("Failed to upload to IPFS")] + PostComputeIpfsUploadFailed, + #[error("Encryption public key is malformed")] + PostComputeMalformedEncryptionPublicKey, + #[error("Failed to zip result folder")] + PostComputeOutFolderZipFailed, + #[error("Empty resultDigest")] + PostComputeResultDigestComputationFailed, + #[error("Result file not found")] + PostComputeResultFileNotFound, + #[error("Failed to send computed file")] + PostComputeSendComputedFileFailed, + #[error("Storage token related environment variable is missing")] + PostComputeStorageTokenMissing, + #[error("Task ID related environment variable is missing")] + PostComputeTaskIdMissing, + #[error("Tee challenge private key related environment variable is missing")] + PostComputeTeeChallengePrivateKeyMissing, + #[error("Result file name too long")] + PostComputeTooLongResultFileName, + #[error("Worker address related environment variable is missing")] + PostComputeWorkerAddressMissing, +} diff --git a/post-compute/src/compute/signer.rs b/post-compute/src/compute/signer.rs new file mode 100644 index 0000000..3dc9701 --- /dev/null +++ b/post-compute/src/compute/signer.rs @@ -0,0 +1,237 @@ +use crate::compute::{ + errors::ReplicateStatusCause, + utils::{ + env_utils::{TeeSessionEnvironmentVariable, get_env_var_or_error}, + hash_utils::{concatenate_and_hash, hex_string_to_byte_array}, + }, +}; +use alloy_signer::{Signature, SignerSync}; +use alloy_signer_local::PrivateKeySigner; + +/// Signs a message hash using the provided enclave challenge private key. +/// +/// This function takes a message hash in hexadecimal string format, converts it to a byte array, +/// and signs it using the provided private key. The resulting signature is then converted back +/// to a string representation. +/// +/// # Arguments +/// +/// * `message_hash` - A hexadecimal string representing the hash to be signed +/// * `enclave_challenge_private_key` - A string containing the private key used for signing +/// +/// # Returns +/// +/// * `Ok(String)` - The signature as a hexadecimal string if successful +/// * `Err(ReplicateStatusCause)` - An error if the private key is invalid or if signing fails +/// +/// # Errors +/// +/// This function will return an error in the following situations: +/// * The provided private key cannot be parsed as a valid `PrivateKeySigner` (returns `PostComputeInvalidEnclaveChallengePrivateKey`) +/// * The signing operation fails (returns `PostComputeInvalidTeeSignature`) +/// +/// # Example +/// +/// ```rust +/// use tee_worker_post_compute::compute::signer::sign_enclave_challenge; +/// +/// let message_hash = "0x5cd0e9c5180dd35e2b8285d0db4ded193a9b4be6fbfab90cbadccecab130acad"; +/// let private_key = "0xdd3b993ec21c71c1f6d63a5240850e0d4d8dd83ff70d29e49247958548c1d479"; +/// +/// match sign_enclave_challenge(message_hash, private_key) { +/// Ok(signature) => println!("Signature: {}", signature), +/// Err(e) => eprintln!("Error: {:?}", e), +/// } +/// ``` +pub fn sign_enclave_challenge( + message_hash: &str, + enclave_challenge_private_key: &str, +) -> Result { + let signer: PrivateKeySigner = enclave_challenge_private_key + .parse::() + .map_err(|_| ReplicateStatusCause::PostComputeInvalidEnclaveChallengePrivateKey)?; + + let signature: Signature = signer + .sign_message_sync(&hex_string_to_byte_array(message_hash)) + .map_err(|_| ReplicateStatusCause::PostComputeInvalidTeeSignature)?; + + Ok(signature.to_string()) +} + +/// Generates a challenge signature for a given chain task ID. +/// +/// This function retrieves the worker address and TEE challenge private key from the environment, +/// then creates a message hash by concatenating and hashing the chain task ID and worker address. +/// Finally, it signs this message hash with the private key. +/// +/// # Arguments +/// +/// * `chain_task_id` - A string identifier for the chain task +/// +/// # Returns +/// +/// * `Ok(String)` - The challenge signature as a hexadecimal string if successful +/// * `Err(ReplicateStatusCause)` - An error if required environment variables are missing or if signing fails +/// +/// # Errors +/// +/// This function will return an error in the following situations: +/// * The worker address environment variable is missing (returns `PostComputeWorkerAddressMissing`) +/// * The TEE challenge private key environment variable is missing (returns `PostComputeTeeChallengePrivateKeyMissing`) +/// * The signing operation fails (returns `PostComputeInvalidTeeSignature`) +/// +/// # Environment Variables +/// +/// * `SIGN_WORKER_ADDRESS` - The worker's address used in message hash calculation +/// * `SIGN_TEE_CHALLENGE_PRIVATE_KEY` - The private key used for signing the challenge +/// +/// # Example +/// +/// ```rust +/// use tee_worker_post_compute::compute::signer::get_challenge; +/// +/// // Assuming the necessary environment variables are set: +/// // SIGN_WORKER_ADDRESS=0xabcdef123456789 +/// // SIGN_TEE_CHALLENGE_PRIVATE_KEY=0xdd3b993ec21c71c1f6d63a5240850e0d4d8dd83ff70d29e49247958548c1d479 +/// +/// let chain_task_id = "0x123456789abcdef"; +/// +/// match get_challenge(chain_task_id) { +/// Ok(signature) => println!("Challenge signature: {}", signature), +/// Err(e) => eprintln!("Error generating challenge: {:?}", e), +/// } +/// ``` +pub fn get_challenge(chain_task_id: &str) -> Result { + let worker_address: String = get_env_var_or_error( + TeeSessionEnvironmentVariable::SignWorkerAddress, + ReplicateStatusCause::PostComputeWorkerAddressMissing, + )?; + let tee_challenge_private_key: String = get_env_var_or_error( + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey, + ReplicateStatusCause::PostComputeTeeChallengePrivateKeyMissing, + )?; + let message_hash: String = concatenate_and_hash(&[chain_task_id, &worker_address]); + sign_enclave_challenge(&message_hash, &tee_challenge_private_key) +} + +#[cfg(test)] +mod tests { + use super::*; + use temp_env::with_vars; + + const CHAIN_TASK_ID: &str = "0x123456789abcdef"; + const WORKER_ADDRESS: &str = "0xabcdef123456789"; + const ENCLAVE_CHALLENGE_PRIVATE_KEY: &str = + "0xdd3b993ec21c71c1f6d63a5240850e0d4d8dd83ff70d29e49247958548c1d479"; + const MESSAGE_HASH: &str = "0x5cd0e9c5180dd35e2b8285d0db4ded193a9b4be6fbfab90cbadccecab130acad"; + const EXPECTED_SIGNATURE: &str = "0xfcc6bce5eb04284c2eb1ed14405b943574343b1abda33628fbf94a374b18dd16541c6ebf63c6943d8643ff03c7aa17f1cb17b0a8d297d0fd95fc914bdd0e85f81b"; + + #[test] + fn should_sign_enclave_challenge() { + let result = sign_enclave_challenge(MESSAGE_HASH, ENCLAVE_CHALLENGE_PRIVATE_KEY); + assert!(result.is_ok(), "Signing should succeed with valid inputs"); + assert_eq!( + result.unwrap(), + EXPECTED_SIGNATURE, + "The signature should match the expected value exactly" + ); + } + + #[test] + fn should_not_sign_enclave_challenge_with_invalid_key() { + let invalid_key = "invalid_private_key"; + let result = sign_enclave_challenge(MESSAGE_HASH, invalid_key); + assert!( + matches!( + result, + Err(err) if err == ReplicateStatusCause::PostComputeInvalidEnclaveChallengePrivateKey + ), + "Should return missing TEE challenge private key error" + ); + } + + #[test] + fn should_get_challenge() { + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::SignWorkerAddress.name(), + Some(WORKER_ADDRESS), + ), + ( + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey.name(), + Some(ENCLAVE_CHALLENGE_PRIVATE_KEY), + ), + ], + || { + let expected_message_hash = concatenate_and_hash(&[CHAIN_TASK_ID, WORKER_ADDRESS]); + let expected_signature = + sign_enclave_challenge(&expected_message_hash, ENCLAVE_CHALLENGE_PRIVATE_KEY) + .unwrap(); + + let result = get_challenge(CHAIN_TASK_ID); + assert!( + result.is_ok(), + "get_challenge should succeed with valid environment variables" + ); + let signature = result.unwrap(); + assert_eq!( + signature, expected_signature, + "The challenge signature should match expected value" + ); + }, + ); + } + + #[test] + fn should_fail_on_missing_worker_address_env_var() { + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::SignWorkerAddress.name(), + None, + ), + ( + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey.name(), + Some(ENCLAVE_CHALLENGE_PRIVATE_KEY), + ), + ], + || { + let result = get_challenge(CHAIN_TASK_ID); + assert!( + matches!( + result, + Err(err) if err == ReplicateStatusCause::PostComputeWorkerAddressMissing + ), + "Should return missing worker address error" + ); + }, + ); + } + + #[test] + fn should_fail_on_missing_private_key_env_var() { + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::SignWorkerAddress.name(), + Some(WORKER_ADDRESS), + ), + ( + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey.name(), + None, + ), + ], + || { + let result = get_challenge(CHAIN_TASK_ID); + assert!( + matches!( + result, + Err(err) if err == ReplicateStatusCause::PostComputeTeeChallengePrivateKeyMissing + ), + "Should return missing private key error" + ); + }, + ); + } +} diff --git a/post-compute/src/compute/utils.rs b/post-compute/src/compute/utils.rs new file mode 100644 index 0000000..7cd4854 --- /dev/null +++ b/post-compute/src/compute/utils.rs @@ -0,0 +1,3 @@ +pub mod env_utils; +pub mod hash_utils; +pub mod result_utils; diff --git a/post-compute/src/compute/utils/env_utils.rs b/post-compute/src/compute/utils/env_utils.rs new file mode 100644 index 0000000..a99f687 --- /dev/null +++ b/post-compute/src/compute/utils/env_utils.rs @@ -0,0 +1,53 @@ +use crate::compute::errors::ReplicateStatusCause; +use std::env; + +pub enum TeeSessionEnvironmentVariable { + IexecTaskId, + ResultEncryption, + ResultEncryptionPublicKey, + ResultStorageCallback, + ResultStorageProvider, + ResultStorageProxy, + ResultStorageToken, + SignTeeChallengePrivateKey, + SignWorkerAddress, + WorkerHostEnvVar, +} + +impl TeeSessionEnvironmentVariable { + pub fn name(&self) -> &str { + match self { + TeeSessionEnvironmentVariable::IexecTaskId => "IEXEC_TASK_ID", + TeeSessionEnvironmentVariable::ResultEncryption => "RESULT_ENCRYPTION", + TeeSessionEnvironmentVariable::ResultEncryptionPublicKey => { + "RESULT_ENCRYPTION_PUBLIC_KEY" + } + TeeSessionEnvironmentVariable::ResultStorageCallback => "RESULT_STORAGE_CALLBACK", + TeeSessionEnvironmentVariable::ResultStorageProvider => "RESULT_STORAGE_PROVIDER", + TeeSessionEnvironmentVariable::ResultStorageProxy => "RESULT_STORAGE_PROXY", + TeeSessionEnvironmentVariable::ResultStorageToken => "RESULT_STORAGE_TOKEN", + TeeSessionEnvironmentVariable::SignTeeChallengePrivateKey => { + "SIGN_TEE_CHALLENGE_PRIVATE_KEY" + } + TeeSessionEnvironmentVariable::SignWorkerAddress => "SIGN_WORKER_ADDRESS", + TeeSessionEnvironmentVariable::WorkerHostEnvVar => "WORKER_HOST", + } + } +} + +pub fn get_env_var_or_error( + env_var: TeeSessionEnvironmentVariable, + status_cause_if_missing: ReplicateStatusCause, +) -> Result { + match get_env_var(env_var) { + val if val.is_empty() => Err(status_cause_if_missing), + val => Ok(val), + } +} + +pub fn get_env_var(env_var: TeeSessionEnvironmentVariable) -> String { + match env::var(env_var.name()) { + Ok(value) => value, + _ => "".to_string(), + } +} diff --git a/post-compute/src/compute/utils/hash_utils.rs b/post-compute/src/compute/utils/hash_utils.rs new file mode 100644 index 0000000..443bf1e --- /dev/null +++ b/post-compute/src/compute/utils/hash_utils.rs @@ -0,0 +1,113 @@ +use sha3::{Digest, Keccak256}; +use sha256::{Sha256Digest, digest}; + +pub fn concatenate_and_hash(hexa_strings: &[&str]) -> String { + let mut hasher = Keccak256::default(); + for hexa_string in hexa_strings { + println!("value {hexa_string}"); + hasher.update(hex_string_to_byte_array(hexa_string)); + } + format!("0x{:x}", hasher.finalize()) +} + +pub fn hex_string_to_byte_array(input: &str) -> Vec { + let clean_input = clean_hex_prefix(input); + let len = clean_input.len(); + if len == 0 { + return vec![]; + } + + let mut data: Vec = vec![]; + let start_idx = if len % 2 != 0 { + let byte = u8::from_str_radix(&clean_input[0..1], 16).expect(""); + data.push(byte); + 1 + } else { + 0 + }; + + for i in (start_idx..len).step_by(2) { + data.push(u8::from_str_radix(&clean_input[i..i + 2], 16).expect("")); + } + + data +} + +pub fn clean_hex_prefix(input: &str) -> &str { + input.strip_prefix("0x").unwrap_or(input) +} + +pub fn sha256(input: D) -> String { + format!("0x{}", digest(input)) +} + +pub fn keccak256(input: &str) -> String { + format!("0x{:x}", Keccak256::digest(input)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hash_one_value() { + let hexa1 = "0x748e091bf16048cb5103E0E10F9D5a8b7fBDd860"; + assert_eq!( + "0x7ec1be13dbade2e3bfde8c2bdf68859dfff4ea620b3340c451ec56b5fa505ab1", + concatenate_and_hash(&[hexa1]) + ) + } + + #[test] + fn hash_two_values() { + let hexa1 = "0x748e091bf16048cb5103E0E10F9D5a8b7fBDd860"; + let hexa2 = "0xd94b63fc2d3ec4b96daf84b403bbafdc8c8517e8e2addd51fec0fa4e67801be8"; + assert_eq!( + "0x9ca8cbf81a285c62778678c874dae13fdc6857566b67a9a825434dd557e18a8d", + concatenate_and_hash(&[hexa1, hexa2]) + ) + } + + #[test] + fn hash_three_values() { + let hexa1 = "0x748e091bf16048cb5103E0E10F9D5a8b7fBDd860"; + let hexa2 = "0xd94b63fc2d3ec4b96daf84b403bbafdc8c8517e8e2addd51fec0fa4e67801be8"; + let hexa3 = "0x9a43BB008b7A657e1936ebf5d8e28e5c5E021596"; + assert_eq!( + "0x54a76d209e8167e1ffa3bde8e3e7b30068423ca9554e1d605d8ee8fd0f165562", + concatenate_and_hash(&[hexa1, hexa2, hexa3]) + ) + } + + #[test] + fn it_removes_prefix() { + assert_eq!( + "54a76d209e8167e1ffa3bde8e3e7b30068423ca9554e1d605d8ee8fd0f165562", + clean_hex_prefix("0x54a76d209e8167e1ffa3bde8e3e7b30068423ca9554e1d605d8ee8fd0f165562") + ) + } + + #[test] + fn it_returns_value_when_no_prefix() { + assert_eq!( + "54a76d209e8167e1ffa3bde8e3e7b30068423ca9554e1d605d8ee8fd0f165562", + clean_hex_prefix("54a76d209e8167e1ffa3bde8e3e7b30068423ca9554e1d605d8ee8fd0f165562") + ) + } + + #[test] + fn get_sha256_digest() { + assert_eq!( + "0xb33845db05fb0822f1f1e3677cc6787b8a1a7a21f3c12f9e97c70cb596222218", + sha256(String::from("utf8String")) + ) + } + + #[test] + fn get_keccak256_digest() { + assert_eq!( + "0x234b7550275b36696569d306216e035df78db522674abecff884c64e7558ef17", + keccak256("utf8String") + ); + } +} diff --git a/post-compute/src/compute/utils/result_utils.rs b/post-compute/src/compute/utils/result_utils.rs new file mode 100644 index 0000000..600e815 --- /dev/null +++ b/post-compute/src/compute/utils/result_utils.rs @@ -0,0 +1,515 @@ +use crate::compute::utils::hash_utils::{concatenate_and_hash, sha256}; +use crate::compute::{computed_file::ComputedFile, utils::hash_utils::keccak256}; +use log::error; +use std::{ + fs::{self, DirEntry}, + io::Error, + path::Path, +}; + +/// Computes the result digest for web3 tasks using keccak256 hashing. +/// +/// This function is used for tasks that involve smart contract callbacks. It computes +/// a keccak256 hash of the callback data, which is the standard hashing algorithm +/// used in Ethereum and other EVM-compatible blockchains. +/// +/// # Arguments +/// +/// * `computed_file` - A reference to the [`ComputedFile`] containing the callback data +/// +/// # Returns +/// +/// * `String` - The keccak256 hash of the callback data in hexadecimal format (prefixed with "0x") +/// or an empty string if the computation fails +/// +/// # Behavior +/// +/// The function will return an empty string in the following cases: +/// * The task ID is missing from the computed file +/// * The callback data is missing, empty, or None +/// +/// # Example +/// +/// ``` +/// use tee_worker_post_compute::compute::{computed_file::ComputedFile, utils::result_utils::compute_web3_result_digest}; +/// +/// let computed_file = ComputedFile { +/// task_id: Some("0x123".to_string()), +/// callback_data: Some("0x0000000000000000000000000000000000000000000000000000000000000001".to_string()), +/// deterministic_output_path: None, +/// result_digest: None, +/// enclave_signature: None, +/// error_message: None, +/// }; +/// +/// let digest = compute_web3_result_digest(&computed_file); +/// println!("Web3 result digest: {}", digest); +/// // Output: Web3 result digest: 0xcb371be217faa47dab94e0d0ff0840c6cbf41645f0dc1a6ae3f34447155a76f3 +/// ``` +pub fn compute_web3_result_digest(computed_file: &ComputedFile) -> String { + if computed_file.task_id.is_none() { + return "".to_string(); + } + + let callback_data = match &computed_file.callback_data { + Some(data) if !data.is_empty() => data, + _ => { + error!( + "Failed to compute_web3_result_digest (callback_data empty) [chainTaskId:{}]", + computed_file.task_id.as_ref().unwrap() + ); + return "".to_string(); + } + }; + + keccak256(callback_data) +} + +/// Computes the result digest for web2 tasks using SHA256 hashing of output files. +/// +/// This function is used for traditional tasks that produce file outputs. It computes +/// a SHA256-based digest of the files in the deterministic output path. The computation +/// method depends on whether the output is a single file or a directory tree. +/// +/// # Arguments +/// +/// * `computed_file` - A reference to the [`ComputedFile`] containing the output path information +/// +/// # Returns +/// +/// * `String` - The SHA256-based digest of the output files in hexadecimal format (prefixed with "0x") +/// or an empty string if the computation fails +/// +/// # Behavior +/// +/// The function will return an empty string in the following cases: +/// * The deterministic output path is missing, empty, or None +/// * The specified path does not exist on the filesystem +/// * File reading or hashing operations fail +/// +/// The digest computation follows these rules: +/// * **Single file**: Direct SHA256 hash of the file content +/// * **Directory**: Combined hash of all files in the directory (sorted by filename for consistency) +/// +/// # Example +/// +/// ``` +/// use tee_worker_post_compute::compute::{computed_file::ComputedFile, utils::result_utils::compute_web2_result_digest}; +/// +/// let computed_file = ComputedFile { +/// task_id: Some("0x123".to_string()), +/// callback_data: None, +/// deterministic_output_path: Some("/iexec_out/results".to_string()), +/// result_digest: None, +/// enclave_signature: None, +/// error_message: None, +/// }; +/// +/// let digest = compute_web2_result_digest(&computed_file); +/// println!("Web2 result digest: {}", digest); +/// ``` +pub fn compute_web2_result_digest(computed_file: &ComputedFile) -> String { + let host_deterministic_output_path = match &computed_file.deterministic_output_path { + Some(path) => { + if path.is_empty() { + error!( + "Failed to compute_web2_result_digest (deterministic_output_path empty) [chainTaskId:{}]", + computed_file.task_id.as_ref().unwrap() + ); + return "".to_string(); + } else { + Path::new(path) + } + } + _ => { + return "".to_string(); + } + }; + + if !host_deterministic_output_path.exists() { + error!( + "Failed to compute_web2_result_digest (host_deterministic_output_path missing) [chainTaskId:{}]", + computed_file.task_id.as_ref().unwrap() + ); + return "".to_string(); + } + + get_file_tree_sha256(host_deterministic_output_path) +} + +/// Computes the SHA256 hash of a single file's content. +/// +/// This function reads the entire content of a file and computes its SHA256 hash. +/// It includes validation to ensure the file exists, is readable, and contains data. +/// +/// # Arguments +/// +/// * `file_path` - A reference to the [`Path`] of the file to hash +/// +/// # Returns +/// +/// * `String` - The SHA256 hash of the file content in hexadecimal format (prefixed with "0x") +/// or an empty string if the operation fails +/// +/// # Behavior +/// +/// The function will return an empty string in the following cases: +/// * The file does not exist or cannot be read +/// * The file is empty (contains no data) +/// * I/O errors occur during file reading +/// +/// # Example +/// +/// ```rust +/// use std::path::Path; +/// use tee_worker_post_compute::compute::utils::result_utils::sha256_file; +/// +/// let file_path = Path::new("/path/to/result.txt"); +/// let hash = sha256_file(&file_path); +/// +/// if !hash.is_empty() { +/// println!("File hash: {}", hash); +/// } else { +/// println!("Failed to compute file hash"); +/// } +/// ``` +pub fn sha256_file(file_path: &Path) -> String { + let data = match fs::read(file_path) { + Ok(data) => { + if data.is_empty() { + error!( + "Null file content [file_path:{}]", + file_path.to_str().unwrap() + ); + return "".to_string(); + } else { + data + } + } + Err(_) => { + error!( + "Failed to read file [file_path:{}]", + file_path.to_str().unwrap() + ); + return "".to_string(); + } + }; + sha256(data) +} + +/// Computes the SHA256-based digest of a file tree (directory or single file). +/// +/// This function provides a unified way to compute digests for both single files and +/// directory trees. For directories, it ensures deterministic results by sorting +/// files alphabetically before computing the combined hash. +/// +/// # Arguments +/// +/// * `file_tree_path` - A reference to the [`Path`] of the file or directory to process +/// +/// # Returns +/// +/// * `String` - The computed digest in hexadecimal format (prefixed with "0x") +/// or an empty string if the operation fails +/// +/// # Behavior +/// +/// The function handles different input types as follows: +/// * **Single file**: Returns the SHA256 hash of the file content +/// * **Directory**: Computes SHA256 hash of each file, then combines all hashes using keccak256 +/// * **Non-existent path**: Returns an empty string +/// * **Empty directory**: Returns an empty string +/// +/// For directories, files are processed in alphabetical order to ensure consistent +/// results across different filesystems and environments. +/// +/// # Example +/// +/// ``` +/// use std::path::Path; +/// use tee_worker_post_compute::compute::utils::result_utils::get_file_tree_sha256; +/// +/// // Single file +/// let file_path = Path::new("/path/to/result.txt"); +/// let file_digest = get_file_tree_sha256(&file_path); +/// +/// // Directory tree +/// let dir_path = Path::new("/path/to/results/"); +/// let tree_digest = get_file_tree_sha256(&dir_path); +/// +/// println!("File digest: {}", file_digest); +/// println!("Tree digest: {}", tree_digest); +/// ``` +pub fn get_file_tree_sha256(file_tree_path: &Path) -> String { + if !file_tree_path.exists() { + return "".to_string(); + } + //file_tree_path points to a leaf, a single file + if !file_tree_path.is_dir() { + return sha256_file(file_tree_path); + } + //file_tree_path points to a tree, with multiple files + let mut entries = match fs::read_dir(file_tree_path) { + Ok(read_dir) => match read_dir.collect::, Error>>() { + Ok(entries) => { + if entries.is_empty() { + return "".to_string(); + } else { + entries + } + } + Err(_) => return "".to_string(), + }, + Err(_) => return "".to_string(), + }; + // /!\ files MUST be sorted to ensure final concatenate_and_hash(..) is always the same (order matters) + entries.sort_by_key(|entry| entry.path()); + + let mut hashes_vec = Vec::new(); + entries.iter().for_each(|entry| { + let path = entry.path(); + let hash = sha256_file(&path); + hashes_vec.push(hash); + }); + let hashes: Vec<&str> = hashes_vec.iter().map(|s| s.as_str()).collect(); + let hashes = hashes.as_slice(); + concatenate_and_hash(hashes) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::tempdir; + + #[test] + fn compute_web3_result_digest_returns_digest_when_valid_input() { + let computed_file = ComputedFile { + task_id: Some("0x123".to_string()), + callback_data: Some( + "0x0000000000000000000000000000000000000000000000000000000000000001".to_string(), + ), + deterministic_output_path: None, + result_digest: None, + enclave_signature: None, + error_message: None, + }; + + let result = compute_web3_result_digest(&computed_file); + + assert_eq!( + result, + "0xcb371be217faa47dab94e0d0ff0840c6cbf41645f0dc1a6ae3f34447155a76f3" + ); + } + + #[test] + fn compute_web3_result_digest_returns_empty_string_when_task_id_is_none() { + let computed_file = ComputedFile { + task_id: None, + callback_data: Some("0xdata".to_string()), + deterministic_output_path: None, + result_digest: None, + enclave_signature: None, + error_message: None, + }; + + let result = compute_web3_result_digest(&computed_file); + + assert_eq!(result, ""); + } + + #[test] + fn compute_web3_result_digest_returns_empty_string_when_callback_data_is_none() { + let computed_file = ComputedFile { + task_id: Some("0x123".to_string()), + callback_data: None, + deterministic_output_path: None, + result_digest: None, + enclave_signature: None, + error_message: None, + }; + + let result = compute_web3_result_digest(&computed_file); + + assert_eq!(result, ""); + } + + #[test] + fn compute_web3_result_digest_returns_empty_string_when_callback_data_is_empty() { + let computed_file = ComputedFile { + task_id: Some("0x123".to_string()), + callback_data: Some("".to_string()), + deterministic_output_path: None, + result_digest: None, + enclave_signature: None, + error_message: None, + }; + + let result = compute_web3_result_digest(&computed_file); + + assert_eq!(result, ""); + } + + #[test] + fn compute_web2_result_digest_returns_digest_when_valid_input() { + let dir = tempdir().unwrap(); + let output_dir = dir.path().join("output"); + fs::create_dir(&output_dir).unwrap(); + + let test_file_path = output_dir.join("test.txt"); + let mut file = fs::File::create(&test_file_path).unwrap(); + file.write_all(b"test content").unwrap(); + + let computed_file = ComputedFile { + task_id: Some("0x123".to_string()), + callback_data: None, + deterministic_output_path: Some(output_dir.to_str().unwrap().to_string()), + result_digest: None, + enclave_signature: None, + error_message: None, + }; + + let result = compute_web2_result_digest(&computed_file); + + assert!(!result.is_empty()); + assert!(result.starts_with("0x")); + } + + #[test] + fn compute_web2_result_digest_returns_empty_string_when_deterministic_output_path_is_none() { + let computed_file = ComputedFile { + task_id: Some("0x123".to_string()), + callback_data: None, + deterministic_output_path: None, + result_digest: None, + enclave_signature: None, + error_message: None, + }; + + let result = compute_web2_result_digest(&computed_file); + + assert_eq!(result, ""); + } + + #[test] + fn compute_web2_result_digest_returns_empty_string_when_deterministic_output_path_is_empty() { + let computed_file = ComputedFile { + task_id: Some("0x123".to_string()), + callback_data: None, + deterministic_output_path: Some("".to_string()), + result_digest: None, + enclave_signature: None, + error_message: None, + }; + + let result = compute_web2_result_digest(&computed_file); + + assert_eq!(result, ""); + } + + #[test] + fn compute_web2_result_digest_returns_empty_string_when_host_deterministic_output_path_does_not_exist() + { + let computed_file = ComputedFile { + task_id: Some("0x123".to_string()), + callback_data: None, + deterministic_output_path: Some("/non_existent_path".to_string()), + result_digest: None, + enclave_signature: None, + error_message: None, + }; + + let result = compute_web2_result_digest(&computed_file); + + assert_eq!(result, ""); + } + + #[test] + fn sha256_file_returns_digest_when_valid_input() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("test.txt"); + + let mut file = fs::File::create(&file_path).unwrap(); + file.write_all(b"test content").unwrap(); + + let result = sha256_file(&file_path); + + assert!(!result.is_empty()); + assert!(result.starts_with("0x")); + } + + #[test] + fn sha256_file_returns_empty_string_when_file_is_empty() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("empty.txt"); + + let mut file = fs::File::create(&file_path).unwrap(); + file.write_all(b"").unwrap(); + + let result = sha256_file(&file_path); + assert!(result.is_empty()); + } + + #[test] + fn sha256_file_returns_empty_string_when_file_does_not_exist() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("nonexistent.txt"); + + let result = sha256_file(&file_path); + + assert_eq!(result, ""); + } + + #[test] + fn get_file_tree_sha256_returns_digest_when_input_is_file() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("test.txt"); + + let mut file = fs::File::create(&file_path).unwrap(); + file.write_all(b"test content").unwrap(); + + let result = get_file_tree_sha256(&file_path); + + assert!(!result.is_empty()); + assert!(result.starts_with("0x")); + } + + #[test] + fn get_file_tree_sha256_returns_digest_when_input_is_directory() { + let dir = tempdir().unwrap(); + + // Create some files in the directory + let file_path1 = dir.path().join("file1.txt"); + let mut file1 = fs::File::create(&file_path1).unwrap(); + file1.write_all(b"content 1").unwrap(); + + let file_path2 = dir.path().join("file2.txt"); + let mut file2 = fs::File::create(&file_path2).unwrap(); + file2.write_all(b"content 2").unwrap(); + + let result = get_file_tree_sha256(dir.path()); + + assert!(!result.is_empty()); + assert!(result.starts_with("0x")); + } + + #[test] + fn get_file_tree_sha256_returns_empty_string_when_path_does_not_exist() { + let dir = tempdir().unwrap(); + let nonexistent_path = dir.path().join("nonexistent"); + + let result = get_file_tree_sha256(&nonexistent_path); + + assert_eq!(result, ""); + } + + #[test] + fn get_file_tree_sha256_returns_empty_string_when_directory_is_empty() { + let dir = tempdir().unwrap(); + + let result = get_file_tree_sha256(dir.path()); + + assert_eq!(result, ""); + } +} diff --git a/post-compute/src/compute/web2_result.rs b/post-compute/src/compute/web2_result.rs new file mode 100644 index 0000000..402f8b8 --- /dev/null +++ b/post-compute/src/compute/web2_result.rs @@ -0,0 +1,1863 @@ +use crate::api::result_proxy_api_client::{ResultModel, ResultProxyApiClient}; +use crate::compute::{ + computed_file::ComputedFile, + dropbox::{DROPBOX_CONTENT_BASE_URL, DropboxService, DropboxUploader}, + encryption::encrypt_data, + errors::ReplicateStatusCause, + utils::env_utils::{TeeSessionEnvironmentVariable, get_env_var, get_env_var_or_error}, +}; +use base64::{Engine as _, engine::general_purpose}; +use log::{debug, error, info}; +#[cfg(test)] +use mockall::automock; +use std::{ + fs::{self, File}, + io::{self, Write}, + path::{Path, PathBuf}, +}; +use walkdir::WalkDir; +use zip::{ZipWriter, write::FileOptions}; + +const SLASH_POST_COMPUTE_TMP: &str = "/post-compute-tmp"; +const RESULT_FILE_NAME_MAX_LENGTH: usize = 31; +const IPFS_RESULT_STORAGE_PROVIDER: &str = "ipfs"; +const DROPBOX_RESULT_STORAGE_PROVIDER: &str = "dropbox"; + +/// Trait defining the interface for Web2 result processing operations. +/// +/// This trait encapsulates all the operations needed to process computation results +/// for Web2 storage systems. It provides a clean abstraction that allows for easy +/// testing through mocking and potential alternative implementations. +/// +/// The trait methods represent the main stages of the result processing workflow: +/// validation, compression, and upload. Each method can be used independently or +/// as part of the complete workflow provided by [`Web2ResultInterface::encrypt_and_upload_result`]. +#[cfg_attr(test, automock)] +pub trait Web2ResultInterface { + fn encrypt_and_upload_result( + &self, + computed_file: &ComputedFile, + ) -> Result<(), ReplicateStatusCause>; + fn check_result_files_name( + &self, + task_id: &str, + iexec_out_path: &str, + ) -> Result<(), ReplicateStatusCause>; + fn zip_iexec_out( + &self, + iexec_out_path: &str, + save_in: &str, + ) -> Result; + fn eventually_encrypt_result( + &self, + in_data_file_path: &str, + ) -> Result; + fn upload_result( + &self, + computed_file: &ComputedFile, + file_to_upload_path: &str, + ) -> Result; + fn upload_to_ipfs_with_iexec_proxy( + &self, + computed_file: &ComputedFile, + base_url: &str, + token: &str, + file_to_upload_path: &str, + ) -> Result; + fn upload_to_dropbox( + &self, + computed_file: &ComputedFile, + token: &str, + file_to_upload_path: &str, + ) -> Result; +} + +/// Production implementation of [`Web2ResultInterface`]. +/// +/// [`Web2ResultService`] provides the concrete implementation of all Web2 result processing +/// operations. It handles the complete workflow from validation through upload, coordinating +/// between different components to ensure reliable result storage. +/// +/// # Example +/// +/// ```rust +/// use tee_worker_post_compute::compute::{ +/// computed_file::ComputedFile, +/// web2_result::{Web2ResultInterface, Web2ResultService}, +/// }; +/// +/// let computed_file = ComputedFile { +/// task_id: Some(String::from("0x123")), +/// result_digest: Some(String::from("0xabc")), +/// enclave_signature: Some(String::from("0xdef")), +/// ..Default::default() +/// }; +/// +/// // Process and upload results +/// match Web2ResultService.encrypt_and_upload_result(&computed_file) { +/// Ok(()) => println!("Results uploaded successfully"), +/// Err(e) => eprintln!("Upload failed: {:?}", e), +/// } +/// ``` +pub struct Web2ResultService; + +impl Web2ResultService { + /// Adds all files from a directory to a ZIP archive. + /// + /// This private method recursively traverses the source directory and adds all + /// regular files to the provided ZIP writer. It maintains the directory structure + /// within the archive and handles various file types appropriately. + /// + /// # File Handling + /// + /// The method: + /// - Includes all regular files in the directory tree + /// - Preserves the relative directory structure + /// - Skips symbolic links to avoid potential security issues + /// - Uses streaming I/O for memory efficiency with large files + /// + /// # Arguments + /// + /// * `zip` - Mutable reference to the ZIP writer + /// * `source_dir` - Path to the source directory to compress + /// * `options` - ZIP file options (compression method, etc.) + /// + /// # Returns + /// + /// * `Ok(())` - All files were successfully added to the archive + /// * `Err(ReplicateStatusCause)` - An error occurred during compression + /// + /// # Errors + /// + /// This method will return [`ReplicateStatusCause::PostComputeOutFolderZipFailed`] if: + /// - A file cannot be opened for reading + /// - An I/O error occurs during file copying + /// - The ZIP writer encounters an error + /// + /// # Note + /// + /// This is an internal helper method used by the public ZIP creation functions. + fn add_directory_to_zip( + &self, + zip: &mut ZipWriter, + source_dir: &Path, + options: FileOptions<()>, + ) -> Result<(), ReplicateStatusCause> { + WalkDir::new(source_dir) + .min_depth(1) + .into_iter() + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.file_type().is_file() && !entry.path_is_symlink()) + .try_for_each(|entry| { + debug!( + "Adding file to zip [file:{}, zip:{}]", + entry.path().display(), + source_dir.display() + ); + + let path = entry.path(); + let relative = path.strip_prefix(source_dir).unwrap(); + + zip.start_file(relative.to_string_lossy(), options) + .map_err(|e| { + error!("Failed to add file to zip: {e}"); + ReplicateStatusCause::PostComputeOutFolderZipFailed + })?; + + let mut file = File::open(path).map_err(|e| { + error!("Failed to open file for zipping: {e}"); + ReplicateStatusCause::PostComputeOutFolderZipFailed + })?; + + io::copy(&mut file, zip).map_err(|e| { + error!("Failed to copy file to zip: {e}"); + ReplicateStatusCause::PostComputeOutFolderZipFailed + })?; + + Ok(()) + }) + } + + /// Internal implementation of the upload_to_dropbox function for uploading to Dropbox with dependency injection. + /// This allows testing with mocked uploaders. + fn upload_to_dropbox_with_uploader( + &self, + computed_file: &ComputedFile, + token: &str, + file_to_upload_path: &str, + uploader: &T, + ) -> Result { + let task_id = computed_file + .task_id + .as_ref() + .ok_or(ReplicateStatusCause::PostComputeTaskIdMissing)?; + let remote_filename = format!("{task_id}.zip"); + let dropbox_path = format!("/results/{remote_filename}"); + + if !Path::new(file_to_upload_path).exists() { + error!("File to upload not found [task_id:{task_id}, path:{file_to_upload_path}]"); + return Err(ReplicateStatusCause::PostComputeResultFileNotFound); + } + + info!( + "Uploading to Dropbox [task_id:{task_id}, local:{file_to_upload_path}, remote:{dropbox_path}]" + ); + + uploader + .upload_file( + token, + file_to_upload_path, + &dropbox_path, + DROPBOX_CONTENT_BASE_URL, + ) + .map_err(|e| { + error!("Dropbox upload failed [task_id:{task_id}, error:{e:?}]"); + e + }) + } +} + +impl Web2ResultInterface for Web2ResultService { + /// Executes the complete result processing workflow. + /// + /// This is the main entry point for processing computation results. It orchestrates + /// the entire workflow including validation, compression, and upload operations. + /// The method name maintains compatibility with the Java implementation, though + /// encryption is not yet implemented. + /// + /// # Arguments + /// + /// * `computed_file` - The [`ComputedFile`] containing task information and metadata + /// + /// # Returns + /// + /// * `Ok(())` - The result was successfully processed and uploaded + /// * `Err(ReplicateStatusCause)` - An error occurred during processing + /// + /// # Errors + /// + /// This method can return various errors depending on the failure point: + /// - [`ReplicateStatusCause::PostComputeTooLongResultFileName`] - Filename validation failed + /// - [`ReplicateStatusCause::PostComputeOutFolderZipFailed`] - Compression failed + /// - [`ReplicateStatusCause::PostComputeIpfsUploadFailed`] - Upload failed + fn encrypt_and_upload_result( + &self, + computed_file: &ComputedFile, + ) -> Result<(), ReplicateStatusCause> { + // check result file names are not too long + self.check_result_files_name(computed_file.task_id.as_ref().unwrap(), "/iexec_out")?; + + // save zip file to the protected region /post-compute-tmp (temporarily) + let zip_path = match self.zip_iexec_out("/iexec_out", SLASH_POST_COMPUTE_TMP) { + Ok(path) => path, + Err(..) => { + error!("zipIexecOut stage failed"); + return Err(ReplicateStatusCause::PostComputeOutFolderZipFailed); + } + }; + + let result_path = self.eventually_encrypt_result(&zip_path)?; + self.upload_result(computed_file, &result_path)?; //TODO Share result link to beneficiary + + // Clean up the temporary zip file + if let Err(e) = fs::remove_file(&zip_path) { + error!("Failed to remove temporary zip file {zip_path}: {e}"); + // We don't return an error here as the upload was successful + }; + + Ok(()) + } + + /// Validates that all result filenames meet the length requirements. + /// + /// This method checks all files in the specified directory to ensure their names + /// don't exceed the maximum allowed length. This validation prevents issues with + /// storage systems that have filename limitations. + /// + /// # Arguments + /// + /// * `task_id` - The task identifier for logging purposes + /// * `iexec_out_path` - Path to the directory containing result files + /// + /// # Returns + /// + /// * `Ok(())` - All filenames are within the allowed length + /// * `Err(ReplicateStatusCause)` - At least one filename exceeds the limit + fn check_result_files_name( + &self, + task_id: &str, + iexec_out_path: &str, + ) -> Result<(), ReplicateStatusCause> { + if !Path::new(iexec_out_path).exists() { + error!("Can't check result files [chain_task_id: {task_id}]"); + return Err(ReplicateStatusCause::PostComputeFailedUnknownIssue); + } + + let long_filenames: Vec<_> = WalkDir::new(iexec_out_path) + .into_iter() + .filter_map(|entry| entry.ok()) // Skip unreadable entries gracefully + .filter(|entry| entry.file_type().is_file()) // Only process files + .filter_map(|entry| { + entry + .file_name() + .to_str() + .filter(|name| name.len() > RESULT_FILE_NAME_MAX_LENGTH) + .map(|name| (String::from(name), entry.path().to_path_buf())) + }) + .collect(); + + for (file_name, path) in &long_filenames { + error!( + "Too long result file name [chain_task_id:{task_id}, file:{}, filename:{file_name}]", + path.display() + ); + } + + if long_filenames.is_empty() { + Ok(()) + } else { + Err(ReplicateStatusCause::PostComputeTooLongResultFileName) + } + } + + /// Compresses the result directory into a ZIP archive. + /// + /// This method creates a compressed archive of all files in the specified directory. + /// The compression uses the DEFLATE algorithm for optimal balance between compression + /// ratio and processing speed. + /// + /// # Arguments + /// + /// * `iexec_out_path` - Path to the directory containing files to compress + /// * `save_in` - Directory where the ZIP file should be saved + /// + /// # Returns + /// + /// * `Ok(String)` - Path to the created ZIP file + /// * `Err(ReplicateStatusCause)` - Compression failed + fn zip_iexec_out( + &self, + iexec_out_path: &str, + save_in: &str, + ) -> Result { + let source_path = Path::new(iexec_out_path); + let zip_file_name = "iexec_out.zip"; + let zip_path = PathBuf::from(save_in).join(zip_file_name); + + let file = File::create(&zip_path).map_err(|e| { + error!("Failed to create zip file: {e}"); + ReplicateStatusCause::PostComputeOutFolderZipFailed + })?; + + let mut zip = ZipWriter::new(file); + let options = FileOptions::default().compression_method(zip::CompressionMethod::Deflated); + self.add_directory_to_zip(&mut zip, source_path, options)?; + zip.finish().map_err(|e| { + error!("Failed to finish zip file: {e}"); + ReplicateStatusCause::PostComputeOutFolderZipFailed + })?; + + info!("Folder zipped [path:{}]", zip_path.display()); + Ok(String::from(zip_path.to_string_lossy())) + } + + /// Conditionally encrypts a result file based on environment configuration. + /// + /// This function checks the `RESULT_ENCRYPTION` environment variable to determine whether + /// result encryption is required. If encryption is disabled, it returns the original file path. + /// If encryption is enabled, it retrieves the beneficiary's RSA public key from the + /// `RESULT_ENCRYPTION_PUBLIC_KEY` environment variable (Base64-encoded PEM), decodes it, + /// and encrypts the input file using hybrid encryption (AES-256-CBC + RSA-2048). + /// The encrypted output is a ZIP archive containing the encrypted data and key. + /// + /// # Arguments + /// + /// * `in_data_file_path` - Path to the file to be (optionally) encrypted. Must be a valid file. + /// + /// # Returns + /// + /// * `Ok(String)` - Path to the encrypted ZIP file if encryption is enabled, or the original file path if not. + /// * `Err(ReplicateStatusCause)` - If environment variables are missing, invalid, or encryption fails. + /// + /// # Errors + /// + /// * Returns an error if: + /// - The `RESULT_ENCRYPTION` environment variable is missing or invalid + /// - The `RESULT_ENCRYPTION_PUBLIC_KEY` is missing, invalid, or not valid Base64/PEM + /// - The encryption operation fails (see [`encrypt_data`]) + /// + /// # Example + /// + /// ```rust + /// use base64::{Engine as _, engine::general_purpose}; + /// use std::env; + /// use tee_worker_post_compute::compute::web2_result::{ + /// Web2ResultInterface, + /// Web2ResultService + /// }; + /// + /// // Set environment variables for encryption + /// unsafe { + /// env::set_var("RESULT_ENCRYPTION", "true"); + /// env::set_var("RESULT_ENCRYPTION_PUBLIC_KEY", general_purpose::STANDARD.encode("-----BEGIN PUBLIC KEY-----...")); + /// } + /// + /// match Web2ResultService.eventually_encrypt_result("/path/to/result.zip") { + /// Ok(path) => println!("Encrypted file at: {}", path), + /// Err(e) => eprintln!("Failed to encrypt result: {e}"), + /// } + /// ``` + fn eventually_encrypt_result( + &self, + in_data_file_path: &str, + ) -> Result { + info!("Encryption stage started"); + let should_encrypt: bool = match get_env_var_or_error( + TeeSessionEnvironmentVariable::ResultEncryption, + ReplicateStatusCause::PostComputeFailedUnknownIssue, //TODO Update this error cause to a more specific one + ) { + Ok(value) => match value.to_lowercase().parse::() { + Ok(parsed_value) => parsed_value, + Err(e) => { + error!( + "Failed to parse RESULT_ENCRYPTION environment variable as a boolean, defaulting to false [callback_env_var:{value}]: {e}" + ); + false + } + }, + Err(e) => { + error!("Failed to get RESULT_ENCRYPTION environment variable"); + return Err(e); + } + }; + + if !should_encrypt { + info!("Encryption stage mode: NO_ENCRYPTION"); + return Ok(in_data_file_path.to_string()); + } + + info!("Encryption stage mode: ENCRYPTION_REQUESTED"); + let beneficiary_rsa_public_key_base64 = get_env_var_or_error( + TeeSessionEnvironmentVariable::ResultEncryptionPublicKey, + ReplicateStatusCause::PostComputeEncryptionPublicKeyMissing, + )?; + + let plain_text_beneficiary_rsa_public_key = + match general_purpose::STANDARD.decode(beneficiary_rsa_public_key_base64) { + Ok(key_bytes) => match String::from_utf8(key_bytes) { + Ok(key_string) => key_string, + Err(e) => { + error!("Decoded key is not valid UTF-8: {e}"); + return Err(ReplicateStatusCause::PostComputeMalformedEncryptionPublicKey); + } + }, + Err(e) => { + error!("Result encryption public key base64 decoding failed: {e}"); + return Err(ReplicateStatusCause::PostComputeMalformedEncryptionPublicKey); + } + }; + + match encrypt_data( + in_data_file_path, + &plain_text_beneficiary_rsa_public_key, + true, + ) { + Ok(file) => { + info!("Encryption stage completed"); + Ok(file) + } + Err(e) => { + error!("Result encryption failed: {e}"); + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + } + } + } + + /// Uploads the compressed result to the configured storage provider. + /// + /// This method handles the upload process to the configured storage system. + /// Currently supports IPFS through the iExec result proxy, with the potential + /// for additional storage providers in the future. + /// + /// # Arguments + /// + /// * `computed_file` - The [`ComputedFile`] containing task metadata + /// * `file_to_upload_path` - Path to the file that should be uploaded + /// + /// # Returns + /// + /// * `Ok(String)` - The storage link where the result was uploaded + /// * `Err(ReplicateStatusCause)` - Upload failed + #[allow(clippy::wildcard_in_or_patterns)] + fn upload_result( + &self, + computed_file: &ComputedFile, + file_to_upload_path: &str, + ) -> Result { + info!("Upload stage started"); + let storage_provider = get_env_var(TeeSessionEnvironmentVariable::ResultStorageProvider); + let storage_token = get_env_var_or_error( + TeeSessionEnvironmentVariable::ResultStorageToken, + ReplicateStatusCause::PostComputeStorageTokenMissing, + )?; + + let result_link = match storage_provider.as_str() { + DROPBOX_RESULT_STORAGE_PROVIDER => { + info!("Upload stage mode: DROPBOX_STORAGE"); + self.upload_to_dropbox(computed_file, &storage_token, file_to_upload_path)? + } + IPFS_RESULT_STORAGE_PROVIDER | _ => { + if storage_provider.is_empty() { + info!( + "Unknown storage provider '{storage_provider}', falling back to IPFS [task_id:{}]", + computed_file.task_id.as_ref().unwrap() + ); + } + info!("Upload stage mode: IPFS_STORAGE"); + let storage_proxy = get_env_var_or_error( + TeeSessionEnvironmentVariable::ResultStorageProxy, + ReplicateStatusCause::PostComputeFailedUnknownIssue, //TODO Define better error + )?; + self.upload_to_ipfs_with_iexec_proxy( + computed_file, + &storage_proxy, + &storage_token, + file_to_upload_path, + )? + } + }; + + info!("Upload stage completed"); + Ok(result_link) + } + + /// Uploads a file to IPFS using the iExec result proxy service. + /// + /// This method specifically handles uploads to IPFS through the iExec result proxy. + /// It creates a [`ResultModel`] with the necessary metadata and sends it to the + /// proxy service for IPFS storage. + /// + /// # Arguments + /// + /// * `computed_file` - The [`ComputedFile`] containing task metadata + /// * `base_url` - The base URL of the result proxy service + /// * `token` - Authentication token for the result proxy + /// * `file_to_upload_path` - Path to the file that should be uploaded + /// + /// # Returns + /// + /// * `Ok(String)` - The IPFS link where the result was stored + /// * `Err(ReplicateStatusCause)` - Upload failed + fn upload_to_ipfs_with_iexec_proxy( + &self, + computed_file: &ComputedFile, + base_url: &str, + token: &str, + file_to_upload_path: &str, + ) -> Result { + let task_id = computed_file.task_id.as_ref().unwrap(); + + let file_to_upload = fs::read(file_to_upload_path).map_err(|e| { + error!( + "Can't upload_to_ipfs_with_iexec_proxy (missing file_path to upload) [task_id:{task_id}, file_to_upload_path:{file_to_upload_path}]: {e}" + ); + ReplicateStatusCause::PostComputeResultFileNotFound + })?; + + let result_model = ResultModel { + chain_task_id: task_id.clone(), + determinist_hash: computed_file.result_digest.as_ref().unwrap().clone(), + enclave_signature: computed_file.enclave_signature.as_ref().unwrap().clone(), + zip: file_to_upload, + ..Default::default() + }; + + let client = ResultProxyApiClient::new(base_url); + match client.upload_to_ipfs(token, &result_model) { + Ok(ipfs_link) => Ok(ipfs_link), + Err(e) => { + error!( + "Can't upload_to_ipfs_with_iexec_proxy (result proxy issue) [task_id:{task_id}]: {e}" + ); + Err(ReplicateStatusCause::PostComputeIpfsUploadFailed) + } + } + } + + /// Uploads a file to Dropbox storage. + /// + /// # Arguments + /// + /// * `computed_file` - The computed file metadata + /// * `token` - The Dropbox access token + /// * `file_to_upload_path` - Path to the local file to upload + /// + /// # Returns + /// + /// * `Ok(String)` - The Dropbox path where the file was uploaded + /// * `Err(ReplicateStatusCause)` - Upload error + fn upload_to_dropbox( + &self, + computed_file: &ComputedFile, + token: &str, + file_to_upload_path: &str, + ) -> Result { + self.upload_to_dropbox_with_uploader( + computed_file, + token, + file_to_upload_path, + &DropboxService, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::compute::dropbox::MockDropboxUploader; + use mockall::predicate::{eq, function}; + use std::os::unix::fs::symlink; + use temp_env::{self, with_vars}; + use tempfile::{NamedTempFile, TempDir, tempdir}; + use wiremock::{ + Mock, MockServer, ResponseTemplate, + matchers::{method, path}, + }; + use zip::ZipArchive; + + fn create_test_computed_file(task_id: &str) -> ComputedFile { + ComputedFile { + task_id: Some(String::from(task_id)), + result_digest: Some(String::from("0xabc123")), + enclave_signature: Some(String::from("0xdef456")), + ..Default::default() + } + } + + // region encrypt_and_upload_result + fn run_encrypt_and_upload_result( + service: &T, + computed_file: &ComputedFile, + ) -> Result<(), ReplicateStatusCause> { + service.check_result_files_name(computed_file.task_id.as_ref().unwrap(), "/iexec_out")?; + let zip_path = match service.zip_iexec_out("/iexec_out", SLASH_POST_COMPUTE_TMP) { + Ok(path) => path, + Err(..) => { + error!("zipIexecOut stage failed"); + return Err(ReplicateStatusCause::PostComputeOutFolderZipFailed); + } + }; + let result_path = service.eventually_encrypt_result(&zip_path)?; + service.upload_result(computed_file, &result_path)?; + Ok(()) + } + + #[test] + fn encrypt_and_upload_result_completes_successfully_when_all_operations_succeed() { + let mut web2_result_mock = MockWeb2ResultInterface::new(); + let computed_file = create_test_computed_file("0x123"); + let zip_path = "/post-compute-tmp/iexec_out.zip"; + + web2_result_mock + .expect_check_result_files_name() + .with(eq("0x123"), eq("/iexec_out")) + .times(1) + .returning(|_, _| Ok(())); + + web2_result_mock + .expect_zip_iexec_out() + .with(eq("/iexec_out"), eq(SLASH_POST_COMPUTE_TMP)) + .times(1) + .returning(move |_, _| Ok(String::from(zip_path))); + + web2_result_mock + .expect_eventually_encrypt_result() + .with(eq(zip_path)) + .times(1) + .returning(|_| Ok(String::from("/post-compute-tmp/iexec_out.zip"))); + + web2_result_mock + .expect_upload_result() + .with(eq(computed_file.clone()), eq(zip_path)) + .times(1) + .returning(|_, _| Ok(String::from("https://ipfs.io/ipfs/QmHash"))); + + let result = run_encrypt_and_upload_result(&web2_result_mock, &computed_file); + assert!(result.is_ok()); + } + + #[test] + fn encrypt_and_upload_result_returns_zip_failed_error_when_zip_creation_fails() { + let mut web2_result_mock = MockWeb2ResultInterface::new(); + let computed_file = create_test_computed_file("0x123"); + + web2_result_mock + .expect_check_result_files_name() + .returning(|_, _| Ok(())); + + web2_result_mock + .expect_zip_iexec_out() + .returning(|_, _| Err(ReplicateStatusCause::PostComputeOutFolderZipFailed)); + + let result = run_encrypt_and_upload_result(&web2_result_mock, &computed_file); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeOutFolderZipFailed) + ); + } + + #[test] + fn encrypt_and_upload_result_returns_error_when_check_files_fails() { + let mut web2_result_mock = MockWeb2ResultInterface::new(); + let computed_file = create_test_computed_file("0x123"); + + web2_result_mock + .expect_check_result_files_name() + .returning(|_, _| Err(ReplicateStatusCause::PostComputeTooLongResultFileName)); + + let result = run_encrypt_and_upload_result(&web2_result_mock, &computed_file); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeTooLongResultFileName) + ); + } + + #[test] + fn encrypt_and_upload_result_returns_error_when_encryption_returns_error() { + let mut web2_result_mock = MockWeb2ResultInterface::new(); + let computed_file = create_test_computed_file("0x123"); + let zip_path = "/post-compute-tmp/iexec_out.zip"; + + web2_result_mock + .expect_check_result_files_name() + .with(eq("0x123"), eq("/iexec_out")) + .times(1) + .returning(|_, _| Ok(())); + + web2_result_mock + .expect_zip_iexec_out() + .with(eq("/iexec_out"), eq(SLASH_POST_COMPUTE_TMP)) + .times(1) + .returning(move |_, _| Ok(String::from(zip_path))); + + web2_result_mock + .expect_eventually_encrypt_result() + .with(eq(zip_path)) + .times(1) + .returning(|_| Err(ReplicateStatusCause::PostComputeEncryptionFailed)); + + let result = run_encrypt_and_upload_result(&web2_result_mock, &computed_file); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + } + + #[test] + fn encrypt_and_upload_result_returns_error_when_upload_fails() { + let mut web2_result_mock = MockWeb2ResultInterface::new(); + let computed_file = create_test_computed_file("0x123"); + let zip_path = "/post-compute-tmp/iexec_out.zip"; + + web2_result_mock + .expect_check_result_files_name() + .returning(|_, _| Ok(())); + + web2_result_mock + .expect_zip_iexec_out() + .returning(move |_, _| Ok(String::from(zip_path))); + + web2_result_mock + .expect_eventually_encrypt_result() + .with(eq(zip_path)) + .times(1) + .returning(move |_| Ok(String::from("/post-compute-tmp/iexec_out.zip"))); + + web2_result_mock + .expect_upload_result() + .returning(|_, _| Err(ReplicateStatusCause::PostComputeIpfsUploadFailed)); + + let result = run_encrypt_and_upload_result(&web2_result_mock, &computed_file); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeIpfsUploadFailed) + ); + } + // endregion + + // region check_result_files_name + #[test] + fn check_result_files_name_returns_ok_when_all_filenames_valid() { + let temp_dir = TempDir::new().unwrap(); + let task_id = "0x0"; + + File::create(temp_dir.path().join("result.txt")).unwrap(); + File::create(temp_dir.path().join("computed.json")).unwrap(); + File::create(temp_dir.path().join("output.log")).unwrap(); + + let result = + Web2ResultService.check_result_files_name(task_id, temp_dir.path().to_str().unwrap()); + assert!(result.is_ok()); + } + + #[test] + fn check_result_files_name_returns_ok_when_directory_empty() { + let temp_dir = TempDir::new().unwrap(); + let task_id = "0x0"; + + let result = + Web2ResultService.check_result_files_name(task_id, temp_dir.path().to_str().unwrap()); + assert!(result.is_ok()); + } + + #[test] + fn check_result_files_name_returns_error_when_filename_too_long() { + let temp_dir = TempDir::new().unwrap(); + let task_id = "0x0"; + + let long_filename = "result-0x0000000000000000000.txt"; + assert!(long_filename.len() > RESULT_FILE_NAME_MAX_LENGTH); + + File::create(temp_dir.path().join(long_filename)).unwrap(); + File::create(temp_dir.path().join("computed.json")).unwrap(); + + let result = + Web2ResultService.check_result_files_name(task_id, temp_dir.path().to_str().unwrap()); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeTooLongResultFileName) + ); + } + + #[test] + fn check_result_files_name_returns_error_when_directory_not_found() { + let task_id = "0x0"; + let non_existent_path = "/dummy/folder/that/doesnt/exist"; + + let result = Web2ResultService.check_result_files_name(task_id, non_existent_path); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeFailedUnknownIssue) + ); + } + + #[test] + fn check_result_files_name_handles_nested_directories_when_checking_files() { + let temp_dir = TempDir::new().unwrap(); + let task_id = "0x0"; + + let sub_dir = temp_dir.path().join("subdir"); + fs::create_dir(&sub_dir).unwrap(); + + File::create(temp_dir.path().join("root.txt")).unwrap(); + + let long_filename = "this_is_a_very_long_filename_exceeding_limit.txt"; + File::create(sub_dir.join(long_filename)).unwrap(); + + let result = + Web2ResultService.check_result_files_name(task_id, temp_dir.path().to_str().unwrap()); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeTooLongResultFileName) + ); + } + + #[test] + fn check_result_files_name_returns_ok_when_max_length_filename() { + let temp_dir = TempDir::new().unwrap(); + let task_id = "0x0"; + + let max_length_filename = "a".repeat(RESULT_FILE_NAME_MAX_LENGTH); + File::create(temp_dir.path().join(&max_length_filename)).unwrap(); + + let result = + Web2ResultService.check_result_files_name(task_id, temp_dir.path().to_str().unwrap()); + assert!(result.is_ok()); + } + // endregion + + // region zip_iexec_out + #[test] + fn zip_iexec_out_creates_zip_file_when_directory_has_content() { + let source_dir = TempDir::new().unwrap(); + let dest_dir = TempDir::new().unwrap(); + + File::create(source_dir.path().join("result.txt")) + .unwrap() + .write_all(b"test content") + .unwrap(); + File::create(source_dir.path().join("data.json")) + .unwrap() + .write_all(b"{\"key\": \"value\"}") + .unwrap(); + + let result = Web2ResultService.zip_iexec_out( + source_dir.path().to_str().unwrap(), + dest_dir.path().to_str().unwrap(), + ); + assert!(result.is_ok()); + + let zip_path = result.unwrap(); + assert!(PathBuf::from(&zip_path).exists()); + assert!(zip_path.ends_with("iexec_out.zip")); + + let metadata = fs::metadata(&zip_path).unwrap(); + assert!(metadata.len() > 0); + } + + #[test] + fn zip_iexec_out_creates_empty_zip_when_directory_is_empty() { + let source_dir = TempDir::new().unwrap(); + let dest_dir = TempDir::new().unwrap(); + + let result = Web2ResultService.zip_iexec_out( + source_dir.path().to_str().unwrap(), + dest_dir.path().to_str().unwrap(), + ); + assert!(result.is_ok()); + + let zip_path = result.unwrap(); + assert!(PathBuf::from(&zip_path).exists()); + + let metadata = fs::metadata(&zip_path).unwrap(); + assert!(metadata.len() > 0); + } + + #[test] + fn zip_iexec_out_maintains_structure_when_directory_has_subdirectories() { + let source_dir = TempDir::new().unwrap(); + let dest_dir = TempDir::new().unwrap(); + + let sub_dir = source_dir.path().join("subdir"); + fs::create_dir(&sub_dir).unwrap(); + let nested_dir = sub_dir.join("nested"); + fs::create_dir(&nested_dir).unwrap(); + + File::create(source_dir.path().join("root.txt")) + .unwrap() + .write_all(b"root file") + .unwrap(); + File::create(sub_dir.join("sub.txt")) + .unwrap() + .write_all(b"sub file") + .unwrap(); + File::create(nested_dir.join("nested.txt")) + .unwrap() + .write_all(b"nested file") + .unwrap(); + + let result = Web2ResultService.zip_iexec_out( + source_dir.path().to_str().unwrap(), + dest_dir.path().to_str().unwrap(), + ); + assert!(result.is_ok()); + + let zip_path = result.unwrap(); + assert!(PathBuf::from(&zip_path).exists()); + + let file = File::open(&zip_path).unwrap(); + let archive = ZipArchive::new(file).unwrap(); + let file_names: Vec = archive.file_names().map(String::from).collect(); + assert!(file_names.contains(&String::from("root.txt"))); + assert!(file_names.contains(&String::from("subdir/sub.txt"))); + assert!(file_names.contains(&String::from("subdir/nested/nested.txt"))); + } + + #[test] + fn zip_iexec_out_returns_error_when_cannot_create_zip_file() { + let source_dir = TempDir::new().unwrap(); + let invalid_dest = "/invalid/path/that/does/not/exist"; + + let result = + Web2ResultService.zip_iexec_out(source_dir.path().to_str().unwrap(), invalid_dest); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeOutFolderZipFailed) + ); + } + + #[test] + #[cfg(unix)] + fn zip_iexec_out_handles_special_files_when_zipping() { + let source_dir = TempDir::new().unwrap(); + let dest_dir = TempDir::new().unwrap(); + + File::create(source_dir.path().join(".hidden")).unwrap(); + File::create(source_dir.path().join("file with spaces.txt")).unwrap(); + File::create(source_dir.path().join("file-with-dashes.log")).unwrap(); + symlink("/tmp/target", source_dir.path().join("symlink")).unwrap(); + + let result = Web2ResultService.zip_iexec_out( + source_dir.path().to_str().unwrap(), + dest_dir.path().to_str().unwrap(), + ); + assert!(result.is_ok()); + + let zip_path = result.unwrap(); + let file = File::open(&zip_path).unwrap(); + let archive = ZipArchive::new(file).unwrap(); + let mut file_names: Vec<&str> = archive.file_names().collect(); + file_names.sort(); + let mut expected = vec![".hidden", "file with spaces.txt", "file-with-dashes.log"]; + expected.sort(); + assert_eq!(file_names, expected); + assert_eq!(archive.len(), 3, "Zip should contain exactly 3 files"); + } + // endregion + + // region eventually_encrypt_result + fn create_temp_file_with_text(content: &str) -> NamedTempFile { + let mut file = NamedTempFile::new().unwrap(); + file.write_all(content.as_bytes()).unwrap(); + file + } + + #[test] + fn eventually_encrypt_result_returns_original_path_when_encryption_disabled() { + let test_file = create_temp_file_with_text("test content"); + let file_path = test_file.path().to_str().unwrap(); + + with_vars( + vec![( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some("false"), + )], + || { + let result = Web2ResultService.eventually_encrypt_result(file_path); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), file_path); + }, + ); + } + + const TEST_RSA_PUBLIC_KEY_PEM: &str = "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUF2clVtUnVMV3UvMm83ci8xSW9ocQp6RkJTUE93T0xYVlJoZjhBUThDcmZnZWRacE1Ld3huWUk4UGJad09oWEpIMzZLZk1UcnhRVjR3aFhlalZqNjdDCjFaMkFMZjBPcC84dXlKY3JuTlhUYXhhVmY0c1Y0RXB0eTBocTNLSGtuU0J0cTBSOENTV1IxeFI4RGNpR1hJaGgKTkllVkZaazZOS291czZ2Tkt6cWZCbDJWMVorRzJ5eEhCLzNiVE0yWjUyMXgxOUZpWUlkUk91TVlwRFRnVXllagpZTll4Vk5CZlVSWmFHcGhPS1FqYThYWkVuSVR1b0toWVpZclc1NVhuVWM5NHQ4TDgrbzgzVmY0OU9oc1JKQStlCk9IOEFSZGhkN3V0c1lwOVBzcko0bFE3d3N5cFhzNWNpQ0Q3T1c4Y3MvbFFEYk9HRHlPZVlMb0pOeUpWQ1lIUWsKSVR4QTluaWE0aU9iNjdaRUN1UkpCVk01aFYreFBzUkRFdlJERnZKRXA0ZXMwbjhJRDcvOW4reEZFNlZJSFpybgpnUUUrYXA0Vm13Qk8xa3d4K2RhZGNvSlNIdUhyU2FXUGpFRUZ0R0RNNmROTzIxTWdNMlZzeDNxSFdpd2NkbFVzCjI3Ym9HMGhyTlp4d2g2UjdHWmJSNDEwcWN1aXQ5TUw1R1ZSQ0QwaFNpd2lFNDJyb09aRkV1ck9KY2x0K3lGVy8KQW9wV3FtYkkvYmxjZ3VEdk5pT21LRTdCNFkycU9sSC9ma0hZbXN1aDAwOFVRT1ZUcXpYbUFtaTlqNzNiejlmeQpuN1RvS3FabUErYTdkS0pYUTdlNXM2b0VHeDc3Wlc0MzZ4SjF4MTg2MkJVVVgxNGdLOWoyTzVzU0RsTzBadTA5CkdiRUFIZlFUb3EyOTBIUENFeTBydWMwQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ=="; + + #[test] + fn eventually_encrypt_result_returns_encrypted_path_when_encryption_enabled_and_key_valid() { + let base_temp = tempdir().expect("Failed to create base_temp"); + let input_dir = base_temp.path().join("input_valid_encrypt"); + fs::create_dir_all(&input_dir).unwrap(); + let input_file_path = input_dir.join("data_to_encrypt.txt"); + fs::write(&input_file_path, "secret stuff").expect("Failed to write data_to_encrypt.txt"); + + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some("true"), + ), + ( + TeeSessionEnvironmentVariable::ResultEncryptionPublicKey.name(), + Some(TEST_RSA_PUBLIC_KEY_PEM), + ), + ], + || { + let result = + Web2ResultService.eventually_encrypt_result(input_file_path.to_str().unwrap()); + assert!( + result.is_ok(), + "eventually_encrypt_result failed: {:?}", + result.err() + ); + let output_zip_path_str = result.unwrap(); + let output_zip_path = Path::new(&output_zip_path_str); + assert!( + output_zip_path.exists(), + "Encrypted zip file should exist at {output_zip_path_str}" + ); + assert_eq!(output_zip_path.file_name().unwrap(), "iexec_out.zip"); + assert_eq!(output_zip_path.parent().unwrap(), input_dir); + let zip_file_reader = + File::open(output_zip_path).expect("Failed to open output zip file for check"); + let mut archive = ZipArchive::new(zip_file_reader) + .expect("Failed to read output zip archive for check"); + assert_eq!( + archive.len(), + 2, + "Encrypted zip archive should contain 2 files. Found: {}", + archive.len() + ); + let expected_encrypted_data_filename = format!( + "{}.aes", + input_file_path.file_name().unwrap().to_str().unwrap() + ); + assert!(archive.by_name(&expected_encrypted_data_filename).is_ok()); + assert!(archive.by_name("aes-key.rsa").is_ok()); + }, + ); + } + + #[test] + fn eventually_encrypt_result_returns_error_when_encryption_env_var_missing() { + let test_file = create_temp_file_with_text("test content"); + let file_path = test_file.path().to_str().unwrap(); + + with_vars( + vec![( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + None::<&str>, + )], + || { + let result = Web2ResultService.eventually_encrypt_result(file_path); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeFailedUnknownIssue) + ); + }, + ); + } + + #[test] + fn eventually_encrypt_result_defaults_to_false_when_invalid_boolean_values_provided() { + let test_file = create_temp_file_with_text("test content"); + let file_path = test_file.path().to_str().unwrap(); + + // Note: Empty string ("") is excluded because it's handled at the env var level + // and still returns an error, while non-empty invalid values default to false + let invalid_values = ["invalid", "yes", "no", "maybe", "2", "-1", "1", "0"]; + + for invalid_value in invalid_values { + with_vars( + vec![( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some(invalid_value), + )], + || { + let result = Web2ResultService.eventually_encrypt_result(file_path); + // Invalid boolean values now default to false (encryption disabled) + // and return the original file path instead of an error + assert!( + result.is_ok(), + "Expected Ok for invalid value '{invalid_value}' but got Err: {result:?}" + ); + assert_eq!( + result.unwrap(), + file_path, + "Should return original file path when defaulting to false" + ); + }, + ); + } + } + + #[test] + fn eventually_encrypt_result_handles_case_insensitive_boolean_values_when_parsing() { + let test_file = create_temp_file_with_text("test content"); + let file_path = test_file.path().to_str().unwrap(); + + // Test case-insensitive true values + let true_values = ["true", "True", "TRUE"]; + for true_value in true_values { + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some(true_value), + ), + ( + TeeSessionEnvironmentVariable::ResultEncryptionPublicKey.name(), + None::<&str>, + ), + ], + || { + let result = Web2ResultService.eventually_encrypt_result(file_path); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeEncryptionPublicKeyMissing) + ); + }, + ); + } + + // Test case-insensitive false values + let false_values = ["false", "False", "FALSE"]; + for false_value in false_values { + with_vars( + vec![( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some(false_value), + )], + || { + let result = Web2ResultService.eventually_encrypt_result(file_path); + assert!( + result.is_ok(), + "Should succeed when encryption disabled for value: {false_value}" + ); + assert_eq!( + result.unwrap(), + file_path, + "Should return original path for value: {false_value}" + ); + }, + ); + } + } + + #[test] + fn eventually_encrypt_result_returns_error_when_public_key_missing() { + let test_file = create_temp_file_with_text("test content"); + let file_path = test_file.path().to_str().unwrap(); + + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some("true"), + ), + ( + TeeSessionEnvironmentVariable::ResultEncryptionPublicKey.name(), + None::<&str>, + ), + ], + || { + let result = Web2ResultService.eventually_encrypt_result(file_path); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeEncryptionPublicKeyMissing) + ); + }, + ); + } + + #[test] + fn eventually_encrypt_result_returns_error_when_public_key_invalid_base64() { + let test_file = create_temp_file_with_text("test content"); + let file_path = test_file.path().to_str().unwrap(); + + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some("true"), + ), + ( + TeeSessionEnvironmentVariable::ResultEncryptionPublicKey.name(), + Some("invalid_base64!@#"), + ), + ], + || { + let result = Web2ResultService.eventually_encrypt_result(file_path); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeMalformedEncryptionPublicKey) + ); + }, + ); + } + + #[test] + fn eventually_encrypt_result_returns_error_when_public_key_invalid_utf8() { + let test_file = create_temp_file_with_text("test content"); + let file_path = test_file.path().to_str().unwrap(); + let invalid_utf8_base64 = + base64::engine::general_purpose::STANDARD.encode([0xFF, 0xFE, 0xFD]); + + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some("true"), + ), + ( + TeeSessionEnvironmentVariable::ResultEncryptionPublicKey.name(), + Some(&invalid_utf8_base64), + ), + ], + || { + let result = Web2ResultService.eventually_encrypt_result(file_path); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeMalformedEncryptionPublicKey) + ); + }, + ); + } + + #[test] + fn eventually_encrypt_result_returns_error_when_public_key_not_a_valid_key() { + let test_file = create_temp_file_with_text("test content"); + let file_path = test_file.path().to_str().unwrap(); + let not_a_key_base64 = + general_purpose::STANDARD.encode("Hello World, this is valid base64 but not a key."); + + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some("true"), + ), + ( + TeeSessionEnvironmentVariable::ResultEncryptionPublicKey.name(), + Some(not_a_key_base64.as_str()), + ), + ], + || { + let result = Web2ResultService.eventually_encrypt_result(file_path); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err, ReplicateStatusCause::PostComputeEncryptionFailed); + }, + ); + } + + #[test] + fn eventually_encrypt_result_returns_error_when_input_file_is_empty() { + let test_file = create_temp_file_with_text(""); + let file_path = test_file.path().to_str().unwrap(); + + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some("true"), + ), + ( + TeeSessionEnvironmentVariable::ResultEncryptionPublicKey.name(), + Some(TEST_RSA_PUBLIC_KEY_PEM), + ), + ], + || { + let result = Web2ResultService.eventually_encrypt_result(file_path); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeEncryptionFailed) + ); + }, + ); + } + + #[test] + fn eventually_encrypt_result_returns_encrypted_path_when_input_file_is_binary() { + let base_temp = tempdir().expect("Failed to create base_temp"); + let input_dir = base_temp.path().join("input_binary_encrypt"); + fs::create_dir_all(&input_dir).unwrap(); + let input_file_path = input_dir.join("data_to_encrypt.bin"); + let binary_content = vec![0, 159, 146, 150, 255, 0, 100, 200, 50, 10, 0, 255]; + fs::write(&input_file_path, &binary_content).expect("Failed to write binary data"); + + with_vars( + vec![ + ( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some("true"), + ), + ( + TeeSessionEnvironmentVariable::ResultEncryptionPublicKey.name(), + Some(TEST_RSA_PUBLIC_KEY_PEM), + ), + ], + || { + let result = + Web2ResultService.eventually_encrypt_result(input_file_path.to_str().unwrap()); + assert!( + result.is_ok(), + "eventually_encrypt_result failed: {:?}", + result.err() + ); + let output_zip_path_str = result.unwrap(); + let output_zip_path = Path::new(&output_zip_path_str); + assert!( + output_zip_path.exists(), + "Encrypted zip file should exist at {output_zip_path_str}" + ); + assert_eq!(output_zip_path.extension().unwrap_or_default(), "zip"); + }, + ); + } + + #[test] + fn eventually_encrypt_result_returns_error_when_env_var_is_empty_string() { + let test_file = create_temp_file_with_text("test content"); + let file_path = test_file.path().to_str().unwrap(); + + with_vars( + vec![( + TeeSessionEnvironmentVariable::ResultEncryption.name(), + Some(""), // Empty string is handled at env var level, not parsing level + )], + || { + let result = Web2ResultService.eventually_encrypt_result(file_path); + // Empty strings are handled by get_env_var_or_error and still return errors + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeFailedUnknownIssue) + ); + }, + ); + } + // endregion + + // region add_directory_to_zip + #[test] + #[cfg(unix)] + fn zip_iexec_out_skips_symlinks_via_add_directory() { + let source_dir = TempDir::new().unwrap(); + let dest_dir = TempDir::new().unwrap(); + + File::create(source_dir.path().join("regular.txt")) + .unwrap() + .write_all(b"content") + .unwrap(); + symlink("/tmp/target", source_dir.path().join("symlink.txt")).unwrap(); + + let result = Web2ResultService.add_directory_to_zip( + &mut ZipWriter::new(File::create(dest_dir.path().join("test.zip")).unwrap()), + source_dir.path(), + FileOptions::default(), + ); + assert!(result.is_ok()); + + let file = File::open(dest_dir.path().join("test.zip")).unwrap(); + let mut archive = ZipArchive::new(file).unwrap(); + assert_eq!(archive.len(), 1); + assert!(archive.by_name("regular.txt").is_ok()); + } + // endregion + + // region upload_result + #[allow(clippy::wildcard_in_or_patterns)] + fn run_upload_result( + service: &T, + computed_file: &ComputedFile, + file_to_upload_path: &str, + ) -> Result { + info!("Upload stage started"); + let storage_provider = get_env_var(TeeSessionEnvironmentVariable::ResultStorageProvider); + let storage_token = get_env_var_or_error( + TeeSessionEnvironmentVariable::ResultStorageToken, + ReplicateStatusCause::PostComputeStorageTokenMissing, + )?; + let result_link = match storage_provider.as_str() { + DROPBOX_RESULT_STORAGE_PROVIDER => { + info!("Upload stage mode: DROPBOX_STORAGE"); + service.upload_to_dropbox(computed_file, &storage_token, file_to_upload_path)? + } + IPFS_RESULT_STORAGE_PROVIDER | _ => { + if storage_provider.is_empty() { + info!( + "Unknown storage provider '{storage_provider}', falling back to IPFS [task_id:{}]", + computed_file.task_id.as_ref().unwrap() + ); + } + info!("Upload stage mode: IPFS_STORAGE"); + let storage_proxy = get_env_var_or_error( + TeeSessionEnvironmentVariable::ResultStorageProxy, + ReplicateStatusCause::PostComputeFailedUnknownIssue, //TODO Define better error + )?; + service.upload_to_ipfs_with_iexec_proxy( + computed_file, + &storage_proxy, + &storage_token, + file_to_upload_path, + )? + } + }; + + info!("Upload stage completed"); + Ok(result_link) + } + + fn run_upload_result_ipfs(provider: &str) { + temp_env::with_vars( + vec![ + ("RESULT_STORAGE_PROVIDER", Some(provider)), + ("RESULT_STORAGE_TOKEN", Some("storageToken")), + ("RESULT_STORAGE_PROXY", Some("https://proxy.example.com")), + ], + || { + let temp_dir = TempDir::new().unwrap(); + let file_path = temp_dir.path().join("test.zip"); + File::create(&file_path) + .unwrap() + .write_all(b"test content") + .unwrap(); + + let mut mock_service = MockWeb2ResultInterface::new(); + let computed_file = create_test_computed_file("0x0"); + let expected_link = "ipfs://QmHash123"; + + mock_service + .expect_upload_to_ipfs_with_iexec_proxy() + .with( + eq(computed_file.clone()), + eq("https://proxy.example.com"), + eq("storageToken"), + function(|path: &str| path.ends_with("test.zip")), + ) + .times(1) + .returning(move |_, _, _, _| Ok(String::from(expected_link))); + + let result = + run_upload_result(&mock_service, &computed_file, file_path.to_str().unwrap()); + assert!(result.is_ok()); + }, + ); + } + + #[test] + fn upload_result_returns_ipfs_link_when_using_ipfs_provider() { + run_upload_result_ipfs("ipfs"); + } + + #[test] + fn upload_result_uses_ipfs_when_provider_not_recognized() { + run_upload_result_ipfs("unknown-provider"); + } + + #[test] + fn upload_result_returns_dropbox_link_when_using_dropbox_provider() { + temp_env::with_vars( + vec![ + ("RESULT_STORAGE_PROVIDER", Some("dropbox")), + ("RESULT_STORAGE_TOKEN", Some("dropboxToken")), + ("RESULT_STORAGE_PROXY", Some("https://proxy.example.com")), + ], + || { + let temp_dir = TempDir::new().unwrap(); + let file_path = temp_dir.path().join("test.zip"); + File::create(&file_path) + .unwrap() + .write_all(b"test content") + .unwrap(); + + let mut mock_service = MockWeb2ResultInterface::new(); + let computed_file = create_test_computed_file("0x0"); + let expected_link = "/results/0x0.zip"; + + mock_service + .expect_upload_to_dropbox() + .with( + eq(computed_file.clone()), + eq("dropboxToken"), + function(|path: &str| path.ends_with("test.zip")), + ) + .times(1) + .returning(move |_, _, _| Ok(String::from(expected_link))); + + let result = + run_upload_result(&mock_service, &computed_file, file_path.to_str().unwrap()); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected_link); + }, + ); + } + + fn run_upload_result_missing_env(missing_var: &str, expected_error: ReplicateStatusCause) { + let mut envs = vec![ + ("RESULT_STORAGE_PROVIDER", Some("ipfs")), + ("RESULT_STORAGE_TOKEN", Some("token")), + ("RESULT_STORAGE_PROXY", Some("proxy")), + ]; + envs.retain(|(k, _)| *k != missing_var); + temp_env::with_vars(envs, || { + let computed_file = create_test_computed_file("0x0"); + let file_path = "fileToUpload.zip"; + + let result = Web2ResultService.upload_result(&computed_file, file_path); + assert_eq!(result, Err(expected_error)); + }); + } + + #[test] + fn upload_result_returns_error_when_storage_token_missing() { + run_upload_result_missing_env( + "RESULT_STORAGE_TOKEN", + ReplicateStatusCause::PostComputeStorageTokenMissing, + ); + } + + #[test] + fn upload_result_returns_error_when_storage_proxy_missing() { + run_upload_result_missing_env( + "RESULT_STORAGE_PROXY", + ReplicateStatusCause::PostComputeFailedUnknownIssue, + ); + } + + #[test] + fn upload_result_defaults_to_ipfs_when_storage_provider_missing() { + temp_env::with_vars( + vec![ + ("RESULT_STORAGE_TOKEN", Some("token")), + ("RESULT_STORAGE_PROXY", Some("proxy")), + ], + || { + let temp_dir = TempDir::new().unwrap(); + let file_path = temp_dir.path().join("fileToUpload.zip"); + File::create(&file_path) + .unwrap() + .write_all(b"test content") + .unwrap(); + let computed_file = create_test_computed_file("0x0"); + + let mut mock_service = MockWeb2ResultInterface::new(); + mock_service + .expect_upload_to_ipfs_with_iexec_proxy() + .with( + eq(computed_file.clone()), + eq("proxy"), + eq("token"), + function(|path: &str| path.ends_with("fileToUpload.zip")), + ) + .times(1) + .returning(|_, _, _, _| Ok("any-result".to_string())); + + let _ = + run_upload_result(&mock_service, &computed_file, file_path.to_str().unwrap()); + }, + ); + } + // endregion + + // region upload_to_ipfs_with_iexec_proxy + async fn actually_upload_to_ipfs_with_iexec_proxy( + computed_file: ComputedFile, + mock_server: MockServer, + file_path: PathBuf, + ) -> Result { + tokio::task::spawn_blocking(move || { + Web2ResultService.upload_to_ipfs_with_iexec_proxy( + &computed_file, + &mock_server.uri(), + "test-token", + file_path.to_str().unwrap(), + ) + }) + .await + .expect("Task panicked") + } + + #[tokio::test] + async fn upload_to_ipfs_with_iexec_proxy_returns_link_when_upload_succeeds() { + let temp_dir = TempDir::new().unwrap(); + let file_path = temp_dir.path().join("fileToUpload.zip"); + let mut file = File::create(&file_path).unwrap(); + file.write_all(b"test zip content").unwrap(); + + let computed_file = ComputedFile { + task_id: Some(String::from("0x0")), + result_digest: Some(String::from("0xdigest")), + enclave_signature: Some(String::from("0xsignature")), + ..Default::default() + }; + + let mock_server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/v1/results")) + .respond_with(ResponseTemplate::new(200).set_body_string("ipfs://QmHash123")) + .mount(&mock_server) + .await; + + let result = + actually_upload_to_ipfs_with_iexec_proxy(computed_file, mock_server, file_path).await; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "ipfs://QmHash123"); + } + + #[test] + fn upload_to_ipfs_with_iexec_proxy_returns_error_when_file_not_found() { + let computed_file = create_test_computed_file("0x0"); + let non_existent_file = "/this/file/does/not/exist"; + let base_url = "http://localhost"; + let token = "IPFS_TOKEN"; + + let result = Web2ResultService.upload_to_ipfs_with_iexec_proxy( + &computed_file, + base_url, + token, + non_existent_file, + ); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeResultFileNotFound) + ); + } + + #[tokio::test] + async fn upload_to_ipfs_with_iexec_proxy_returns_error_when_api_request_fails() { + let temp_dir = TempDir::new().unwrap(); + let file_path = temp_dir.path().join("fileToUpload.zip"); + File::create(&file_path) + .unwrap() + .write_all(b"test content") + .unwrap(); + let computed_file = create_test_computed_file("0x0"); + + let mock_server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/v1/results")) + .respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error")) + .mount(&mock_server) + .await; + + let result = + actually_upload_to_ipfs_with_iexec_proxy(computed_file, mock_server, file_path).await; + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeIpfsUploadFailed) + ); + } + // endregion + + // region upload_to_dropbox + #[test] + fn upload_to_dropbox_returns_error_when_task_id_missing() { + let computed_file = ComputedFile { + task_id: None, + ..Default::default() + }; + + let result = Web2ResultService.upload_to_dropbox(&computed_file, "token", "/no/file"); + assert_eq!(result, Err(ReplicateStatusCause::PostComputeTaskIdMissing)); + } + + #[test] + fn upload_to_dropbox_returns_error_when_file_not_found() { + let computed_file = create_test_computed_file("0xdeadbeef"); + + let result = + Web2ResultService.upload_to_dropbox(&computed_file, "token", "/path/does/not/exist"); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeResultFileNotFound) + ); + } + + #[test] + fn upload_to_dropbox_returns_error_when_local_path_is_directory() { + let computed_file = create_test_computed_file("0xdir"); + let temp_dir = TempDir::new().unwrap(); + + let result = Web2ResultService.upload_to_dropbox( + &computed_file, + "token", + temp_dir.path().to_str().unwrap(), + ); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeDropboxUploadFailed) + ); + } + + #[test] + fn upload_to_dropbox_returns_ok_when_upload_succeeds() { + let temp_file = NamedTempFile::new().unwrap(); + fs::write(temp_file.path(), b"content").unwrap(); + let computed_file = create_test_computed_file("0xsucc"); + let file_path = temp_file.path().to_str().unwrap().to_string(); + + let mut mock_uploader = MockDropboxUploader::new(); + mock_uploader + .expect_upload_file() + .with( + eq("test-token"), + eq(file_path.clone()), + eq("/results/0xsucc.zip"), + eq(DROPBOX_CONTENT_BASE_URL), + ) + .times(1) + .returning(|_, _, _, _| Ok("/results/0xsucc.zip".to_string())); + + let result = Web2ResultService.upload_to_dropbox_with_uploader( + &computed_file, + "test-token", + &file_path, + &mock_uploader, + ); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "/results/0xsucc.zip"); + } + + #[test] + fn upload_to_dropbox_propagates_error_when_upload_fails() { + let temp_file = NamedTempFile::new().unwrap(); + fs::write(temp_file.path(), b"content").unwrap(); + let computed_file = create_test_computed_file("0xerr"); + let file_path = temp_file.path().to_str().unwrap().to_string(); + + let mut mock_uploader = MockDropboxUploader::new(); + mock_uploader + .expect_upload_file() + .with( + eq("test-token"), + eq(file_path.clone()), + eq("/results/0xerr.zip"), + eq(DROPBOX_CONTENT_BASE_URL), + ) + .times(1) + .returning(|_, _, _, _| Err(ReplicateStatusCause::PostComputeDropboxUploadFailed)); + + let result = Web2ResultService.upload_to_dropbox_with_uploader( + &computed_file, + "test-token", + &file_path, + &mock_uploader, + ); + assert_eq!( + result, + Err(ReplicateStatusCause::PostComputeDropboxUploadFailed) + ); + } + // endregion + + // region add_directory_to_zip + #[test] + fn add_directory_to_zip_adds_files_correctly() { + let source_dir = TempDir::new().unwrap(); + let dest_dir = TempDir::new().unwrap(); + + File::create(source_dir.path().join("file1.txt")) + .unwrap() + .write_all(b"content1") + .unwrap(); + + let sub_dir = source_dir.path().join("subdir"); + fs::create_dir(&sub_dir).unwrap(); + File::create(sub_dir.join("file2.txt")) + .unwrap() + .write_all(b"content2") + .unwrap(); + + let result = Web2ResultService.add_directory_to_zip( + &mut ZipWriter::new(File::create(dest_dir.path().join("test.zip")).unwrap()), + source_dir.path(), + FileOptions::default(), + ); + assert!(result.is_ok()); + + let file = File::open(dest_dir.path().join("test.zip")).unwrap(); + let archive = ZipArchive::new(file).unwrap(); + let mut file_names: Vec<&str> = archive.file_names().collect(); + file_names.sort(); + let mut expected_file_names = vec!["file1.txt", "subdir/file2.txt"]; + expected_file_names.sort(); + assert_eq!(file_names, expected_file_names); + } + + #[test] + #[cfg(unix)] + fn add_directory_to_zip_skips_symlinks() { + let source_dir = TempDir::new().unwrap(); + let dest_dir = TempDir::new().unwrap(); + + File::create(source_dir.path().join("regular.txt")) + .unwrap() + .write_all(b"content") + .unwrap(); + symlink("/tmp/target", source_dir.path().join("symlink.txt")).unwrap(); + + let result = Web2ResultService.add_directory_to_zip( + &mut ZipWriter::new(File::create(dest_dir.path().join("test.zip")).unwrap()), + source_dir.path(), + FileOptions::default(), + ); + assert!(result.is_ok()); + + let file = File::open(dest_dir.path().join("test.zip")).unwrap(); + let mut archive = ZipArchive::new(file).unwrap(); + assert_eq!(archive.len(), 1); + assert!(archive.by_name("regular.txt").is_ok()); + } + // endregion +} diff --git a/post-compute/src/lib.rs b/post-compute/src/lib.rs new file mode 100644 index 0000000..a56b8f4 --- /dev/null +++ b/post-compute/src/lib.rs @@ -0,0 +1,2 @@ +pub mod api; +pub mod compute;