diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c1cf89..0a31cd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,4 +188,4 @@ jobs: key: nightly-${{ hashFiles('Cargo.lock') }} - name: Install just uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 - - run: just check-format + - run: just check-deny diff --git a/Cargo.lock b/Cargo.lock index 4caa85d..5f29d9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -30,9 +42,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1958f0294ecc05ebe7b3c9a8662a3e221c2523b7f2bcd94c7a651efbd510bf" +checksum = "86debde32d8dbb0ab29e7cc75ae1a98688ac7a4c9da54b3a9b14593b9b3c46d3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -57,9 +69,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f752e99497ddc39e22d547d7dfe516af10c979405a034ed90e69b914b7dddeae" +checksum = "8d6cb2e7efd385b333f5a77b71baaa2605f7e22f1d583f2879543b54cbce777c" dependencies = [ "alloy-consensus", "alloy-eips", @@ -103,6 +115,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "borsh", + "k256", "serde", "thiserror", ] @@ -121,9 +134,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "813a67f87e56b38554d18b182616ee5006e8e2bf9df96a0df8bf29dff1d52e3f" +checksum = "be47bf1b91674a5f394b9ed3c691d764fb58ba43937f1371550ff4bc8e59c295" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -157,13 +170,13 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dd146b3de349a6ffaa4e4e319ab3a90371fb159fb0bddeb1c7bbe8b1792eff" +checksum = "5a24c81a56d684f525cd1c012619815ad3a1dd13b0238f069356795d84647d3c" dependencies = [ "alloy-primitives", "alloy-sol-types", - "http", + "http 1.4.0", "serde", "serde_json", "thiserror", @@ -172,9 +185,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c12278ffbb8872dfba3b2f17d8ea5e8503c2df5155d9bc5ee342794bde505c3" +checksum = "786c5b3ad530eaf43cda450f973fe7fb1c127b4c8990adf66709dafca25e3f6f" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -198,9 +211,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833037c04917bc2031541a60e8249e4ab5500e24c637c1c62e95e963a655d66f" +checksum = "c1ed40adf21ae4be786ef5eb62db9c692f6a30f86d34452ca3f849d6390ce319" dependencies = [ "alloy-consensus", "alloy-eips", @@ -220,7 +233,7 @@ dependencies = [ "cfg-if", "const-hex", "derive_more", - "foldhash", + "foldhash 0.2.0", "hashbrown 0.16.1", "indexmap 2.13.0", "itoa", @@ -238,9 +251,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eafa840b0afe01c889a3012bb2fde770a544f74eab2e2870303eb0a5fb869c48" +checksum = "a3ca4c15818be7ac86208aff3a91b951d14c24e1426e66624e75f2215ba5e2cc" dependencies = [ "alloy-chains", "alloy-consensus", @@ -263,7 +276,7 @@ dependencies = [ "either", "futures", "futures-utils-wasm", - "lru", + "lru 0.16.3", "parking_lot", "pin-project", "reqwest", @@ -278,9 +291,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" +checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -289,9 +302,9 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" +checksum = "ce8849c74c9ca0f5a03da1c865e3eb6f768df816e67dd3721a398a8a7e398011" dependencies = [ "proc-macro2", "quote", @@ -300,9 +313,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12768ae6303ec764905a8a7cd472aea9072f9f9c980d18151e26913da8ae0123" +checksum = "abe0addad5b8197e851062b49dc47157444bced173b601d91e3f9b561a060a50" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -323,11 +336,12 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0622d8bcac2f16727590aa33f4c3f05ea98130e7e4b4924bce8be85da5ad0dae" +checksum = "74d17d4645a717f0527e491f44f6f7a75c221b9c00ccf79ddba2d26c8e0df4c3" dependencies = [ "alloy-primitives", + "alloy-rpc-types-eth", "alloy-rpc-types-txpool", "alloy-serde", "serde", @@ -335,9 +349,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1cf5a093e437dfd62df48e480f24e1a3807632358aad6816d7a52875f1c04aa" +checksum = "d0e98aabb013a71a4b67b52825f7b503e5bb6057fb3b7b2290d514b0b0574b57" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -346,9 +360,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e97603095020543a019ab133e0e3dc38cd0819f19f19bdd70c642404a54751" +checksum = "5899af8417dcf89f40f88fa3bdb2f3f172605d8e167234311ee34811bbfdb0bf" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -367,9 +381,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b73c1d6e4f1737a20d246dad5a0abd6c1b76ec4c3d153684ef8c6f1b6bb4f4" +checksum = "3a8074654c0292783d504bfa1f2691a69f420154ee9a7883f9212eaf611e60cd" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -379,9 +393,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946a0d413dbb5cd9adba0de5f8a1a34d5b77deda9b69c1d7feed8fc875a1aa26" +checksum = "feb73325ee881e42972a5a7bc85250f6af89f92c6ad1222285f74384a203abeb" dependencies = [ "alloy-primitives", "serde", @@ -390,9 +404,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7481dc8316768f042495eaf305d450c32defbc9bce09d8bf28afcd956895bb" +checksum = "1bea4c8f30eddb11d7ab56e83e49c814655daa78ca708df26c300c10d0189cbc" dependencies = [ "alloy-primitives", "async-trait", @@ -403,6 +417,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "alloy-signer-local" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28bd71507db58477151a6fe6988fa62a4b778df0f166c3e3e1ef11d059fe5fa" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "k256", + "rand 0.8.5", + "thiserror", +] + [[package]] name = "alloy-sol-macro" version = "1.5.4" @@ -475,13 +505,13 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f169b85eb9334871db986e7eaf59c58a03d86a30cc68b846573d47ed0656bb" +checksum = "b321f506bd67a434aae8e8a7dfe5373bf66137c149a5f09c9e7dfb0ca43d7c91" dependencies = [ "alloy-json-rpc", "auto_impl", - "base64", + "base64 0.22.1", "derive_more", "futures", "futures-utils-wasm", @@ -498,9 +528,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "019821102e70603e2c141954418255bec539ef64ac4117f8e84fb493769acf73" +checksum = "30bf12879a20e1261cd39c3b101856f52d18886907a826e102538897f0d2b66e" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -516,9 +546,9 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428aa0f0e0658ff091f8f667c406e034b431cb10abd39de4f507520968acc499" +checksum = "4d7fd448ab0a017de542de1dcca7a58e7019fe0e7a34ed3f9543ebddf6aceffa" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -527,14 +557,15 @@ dependencies = [ "nybbles", "serde", "smallvec", + "thiserror", "tracing", ] [[package]] name = "alloy-tx-macros" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ceac797eb8a56bdf5ab1fab353072c17d472eab87645ca847afe720db3246d" +checksum = "6a91d6b4c2f6574fdbcb1611e460455c326667cf5b805c6bd1640dad8e8ee4d2" dependencies = [ "darling", "proc-macro2", @@ -551,56 +582,12 @@ dependencies = [ "libc", ] -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - [[package]] name = "anyhow" version = "1.0.100" @@ -861,24 +848,484 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-config" +version = "1.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c456581cb3c77fafcc8c67204a70680d40b61112d6da78c77bd31d945b65f1b5" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http 0.63.3", + "aws-smithy-json 0.62.3", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.4.0", + "time", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c635c2dc792cb4a11ce1a4f392a925340d1bdf499289b5ec1ec6810954eb43f5" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http 0.63.3", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.119.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d65fddc3844f902dfe1864acb8494db5f9342015ee3ab7890270d36fbd2e01c" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http 0.62.6", + "aws-smithy-json 0.61.9", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "lru 0.12.5", + "percent-encoding", + "regex-lite", + "sha2", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6443ccadc777095d5ed13e21f5c364878c9f5bad4e35187a6cdbd863b0afcad" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http 0.63.3", + "aws-smithy-json 0.62.3", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa49f3c607b92daae0c078d48a4571f599f966dce3caee5f1ea55c4d9073f99" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http 0.63.3", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "percent-encoding", + "sha2", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52eec3db979d18cb807fc1070961cc51d87d069abe9ab57917769687368a8c6c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.63.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87294a084b43d649d967efe58aa1f9e0adc260e13a6938eb904c0ae9b45824ae" +dependencies = [ + "aws-smithy-http 0.62.6", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 0.2.12", + "http-body 0.4.6", + "md-5", + "pin-project-lite", + "sha1", + "sha2", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b9c7354a3b13c66f60fe4616d6d1969c9fd36b1b5333a5dfb3ee716b33c588" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.62.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630e67f2a31094ffa51b210ae030855cb8f3b7ee1329bdd8d085aaf61e8b97fc" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fb0abf49ff0cab20fd31ac1215ed7ce0ea92286ba09e2854b42ba5cabe7525" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.13", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.8.1", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.36", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb96aa208d62ee94104645f7b2ecaf77bf27edf161590b6224bfbac2832f979" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a46543fbc94621080b3cf553eb4cbbdc41dd9780a30c4756400f0139440a1d" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cebbddb6f3a5bd81553643e9c7daf3cc3dc5b0b5f398ac668630e8a84e6fff0" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3df87c14f0127a0d77eb261c3bc45d5b4833e2a1f63583ebfb728e4852134ee" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http 0.63.3", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49952c52f7eebb72ce2a754d3866cc0f87b97d2a46146b79f80f3a93fb2b3716" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3a26048eeab0ddeba4b4f9d51654c79af8c3b32357dc5f336cee85ab331c33" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version 0.4.1", + "tracing", +] + +[[package]] +name = "base-bundles" +version = "0.0.0" +source = "git+https://github.com/base/base?branch=main#50e78bde5073eb365e9d09e9f61c7e813d0f55c2" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-provider", + "alloy-serde", + "op-alloy-consensus 0.23.1", + "op-alloy-flz", + "serde", + "uuid", +] + [[package]] name = "base16ct" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.114", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -910,6 +1357,12 @@ dependencies = [ "hex-conservative", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -949,6 +1402,56 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bollard" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30" +dependencies = [ + "base64 0.22.1", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "home", + "http 1.4.0", + "http-body-util", + "hyper 1.8.1", + "hyper-named-pipe", + "hyper-rustls 0.27.7", + "hyper-util", + "hyperlocal", + "log", + "pin-project-lite", + "rustls 0.23.36", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.47.1-rc.27.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" +dependencies = [ + "serde", + "serde_repr", + "serde_with", +] + [[package]] name = "borsh" version = "1.6.0" @@ -992,13 +1495,23 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "c-kzg" version = "2.1.5" @@ -1025,14 +1538,25 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.54" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -1057,11 +1581,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" -version = "4.5.56" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", @@ -1069,14 +1604,12 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.56" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ - "anstream", "anstyle", "clap_lex", - "strsim", ] [[package]] @@ -1098,10 +1631,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] -name = "colorchoice" -version = "1.0.4" +name = "cmake" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] [[package]] name = "const-hex" @@ -1160,6 +1696,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1191,10 +1737,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] -name = "crossbeam-channel" -version = "0.5.15" +name = "crc-fast" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" +dependencies = [ + "crc", + "digest 0.10.7", + "rand 0.9.2", + "regex", + "rustversion", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] @@ -1369,6 +1946,17 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "docker_credential" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1479,6 +2067,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1517,11 +2116,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixed-hash" @@ -1541,6 +2151,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -1571,6 +2187,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -1723,6 +2345,44 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1735,6 +2395,17 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -1743,7 +2414,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", "serde", "serde_core", ] @@ -1784,6 +2455,26 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.4.0" @@ -1794,6 +2485,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1801,7 +2503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.4.0", ] [[package]] @@ -1812,8 +2514,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -1823,6 +2525,36 @@ 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 = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.8.1" @@ -1833,9 +2565,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http", - "http-body", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1844,6 +2578,53 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper 1.8.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.36", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1852,7 +2633,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-util", "native-tls", "tokio", @@ -1862,28 +2643,42 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", - "http", - "http-body", - "hyper", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.2", "tokio", "tower-service", "tracing", ] +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -2075,12 +2870,6 @@ dependencies = [ "serde", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - [[package]] name = "itertools" version = "0.10.5" @@ -2114,6 +2903,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -2169,12 +2968,45 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.7.0", +] + +[[package]] +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -2202,6 +3034,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "lru" version = "0.16.3" @@ -2232,31 +3073,104 @@ dependencies = [ ] [[package]] -name = "memchr" -version = "2.7.6" +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mempool-rebroadcaster" +version = "0.1.0" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types", + "alloy-rpc-types-eth", + "alloy-trie", + "clap", + "dotenvy", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "metrics" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +dependencies = [ + "ahash", + "portable-atomic", +] + +[[package]] +name = "metrics-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a87f4b19620e4c561f7b48f5e6ca085b1780def671696a6a3d9d0c137360ec" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b166dea96003ee2531cf14833efedced545751d800f03535801d833313f8c15" +dependencies = [ + "base64 0.22.1", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "indexmap 2.13.0", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-util" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "mempool-rebroadcaster" -version = "0.1.0" +checksum = "cdfb1365fea27e6dd9dc1dbc19f570198bc86914533ad639dae939635f096be4" dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types", - "alloy-rpc-types-eth", - "alloy-trie", - "clap", - "dotenvy", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-subscriber", + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.16.1", + "metrics", + "quanta", + "rand 0.9.2", + "rand_xoshiro", + "sketches-ddsketch", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.1.1" @@ -2277,14 +3191,24 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2355,6 +3279,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.114", @@ -2362,9 +3287,9 @@ dependencies = [ [[package]] name = "nybbles" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5676b5c379cf5b03da1df2b3061c4a4e2aa691086a56ac923e08c143f53f59" +checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" dependencies = [ "alloy-rlp", "cfg-if", @@ -2381,10 +3306,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "op-alloy-consensus" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726da827358a547be9f1e37c2a756b9e3729cb0350f43408164794b370cad8ae" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "derive_more", + "serde", + "thiserror", +] + +[[package]] +name = "op-alloy-consensus" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736381a95471d23e267263cfcee9e1d96d30b9754a94a2819148f83379de8a86" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "derive_more", + "serde", + "thiserror", +] + +[[package]] +name = "op-alloy-flz" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" + +[[package]] +name = "op-alloy-rpc-types" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "562dd4462562c41f9fdc4d860858c40e14a25df7f983ae82047f15f08fce4d19" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "derive_more", + "op-alloy-consensus 0.22.4", + "serde", + "serde_json", + "thiserror", +] [[package]] name = "openssl" @@ -2392,7 +3368,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -2418,6 +3394,21 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.5.5+3.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.111" @@ -2426,6 +3417,7 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -2452,10 +3444,16 @@ checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", - "http", + "http 1.4.0", "opentelemetry", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -2502,11 +3500,36 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn 2.0.114", +] + [[package]] name = "paste" version = "1.0.15" @@ -2577,6 +3600,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.4" @@ -2660,7 +3689,7 @@ checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ "bit-set", "bit-vec", - "bitflags", + "bitflags 2.10.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -2671,6 +3700,21 @@ dependencies = [ "unarray", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -2769,6 +3813,15 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "rapidhash" version = "4.2.1" @@ -2778,13 +3831,72 @@ dependencies = [ "rustversion", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "rdkafka" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b52c81ac3cac39c9639b95c20452076e74b8d9a71bc6fc4d83407af2ea6fff" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "log", + "rdkafka-sys", + "serde", + "serde_derive", + "serde_json", + "slab", + "tokio", +] + +[[package]] +name = "rdkafka-sys" +version = "4.10.0+2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e234cf318915c1059d4921ef7f75616b5219b10b46e9f3a511a15eb4b56a3f77" +dependencies = [ + "libc", + "libz-sys", + "num_enum", + "openssl-sys", + "pkg-config", + "zstd-sys", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.10.0", ] [[package]] @@ -2807,22 +3919,40 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reqwest" @@ -2830,13 +3960,13 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-tls", "hyper-util", "js-sys", @@ -2870,6 +4000,20 @@ dependencies = [ "subtle", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rlp" version = "0.5.2" @@ -2950,13 +4094,61 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.9", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -2966,6 +4158,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3013,9 +4227,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -3029,6 +4243,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sec1" version = "0.7.3" @@ -3071,8 +4295,21 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", - "core-foundation", + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3155,6 +4392,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3173,13 +4421,13 @@ version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ - "base64", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.2.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -3208,6 +4456,23 @@ dependencies = [ "serde", ] +[[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 = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -3292,11 +4557,17 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "sketches-ddsketch" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" + [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3307,6 +4578,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.2" @@ -3345,6 +4626,29 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.114", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "strum" version = "0.27.2" @@ -3445,6 +4749,44 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "testcontainers" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a4f01f39bb10fc2a5ab23eb0d888b1e2bb168c157f61a1b98e6c501c639c74" +dependencies = [ + "async-trait", + "bollard", + "bollard-stubs", + "bytes", + "docker_credential", + "either", + "etcetera", + "futures", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror", + "tokio", + "tokio-stream", + "tokio-tar", + "tokio-util", + "url", +] + +[[package]] +name = "testcontainers-modules" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d43ed4e8f58424c3a2c6c56dbea6643c3c23e8666a34df13c54f0a184e6c707" +dependencies = [ + "testcontainers", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -3524,6 +4866,66 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tips-audit" +version = "0.0.0" +dependencies = [ + "anyhow", + "aws-config", + "aws-credential-types", + "aws-sdk-s3", + "clap", + "dotenvy", + "rdkafka", + "tips-audit-lib", + "tips-core", + "tokio", + "tracing", +] + +[[package]] +name = "tips-audit-lib" +version = "0.0.0" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "anyhow", + "async-trait", + "aws-config", + "aws-sdk-s3", + "bytes", + "futures", + "metrics", + "metrics-derive", + "rdkafka", + "serde", + "serde_json", + "testcontainers", + "testcontainers-modules", + "tips-core", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "tips-core" +version = "0.0.0" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types", + "alloy-signer-local", + "base-bundles", + "metrics-exporter-prometheus", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types", + "serde_json", + "tracing", + "tracing-subscriber", +] + [[package]] name = "tokio" version = "1.49.0" @@ -3536,7 +4938,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -3562,6 +4964,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.36", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -3574,6 +4996,21 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tar" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall 0.3.5", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3638,11 +5075,11 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "iri-string", "pin-project-lite", "tower", @@ -3804,6 +5241,12 @@ 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.8" @@ -3817,6 +5260,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3824,10 +5273,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "utf8parse" -version = "0.2.2" +name = "uuid" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] [[package]] name = "valuable" @@ -3847,6 +5303,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "wait-timeout" version = "0.2.1" @@ -3973,6 +5435,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -4032,13 +5516,31 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -4050,6 +5552,37 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -4057,58 +5590,148 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -4145,6 +5768,22 @@ dependencies = [ "tap", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yoke" version = "0.8.1" @@ -4170,18 +5809,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.36" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.36" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" dependencies = [ "proc-macro2", "quote", @@ -4264,6 +5903,17 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "bindgen", + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 4e07355..4d471c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,35 +1,149 @@ +[workspace.package] +version = "0.0.0" +edition = "2024" +rust-version = "1.88" +license = "MIT" +homepage = "https://github.com/base/infra" +repository = "https://github.com/base/infra" +exclude = [".github/"] + [workspace] resolver = "2" -members = ["crates/mempool-rebroadcaster", "crates/sidecrush"] +members = [ + "bin/tips/*", + "crates/mempool-rebroadcaster", + "crates/sidecrush", + "crates/tips/*", +] +default-members = ["bin/tips/audit"] + +[workspace.lints.rust] +missing-debug-implementations = "warn" +missing-docs = "warn" +unreachable-pub = "warn" +unused-must-use = "deny" +rust-2018-idioms = "deny" +unnameable-types = "warn" + +[workspace.lints.rustdoc] +all = "warn" + +[workspace.lints.clippy] +# Baseline - enable all default clippy lints +all = { level = "warn", priority = -1 } + +# === Performance & Cloning === +missing-const-for-fn = "warn" +redundant-clone = "warn" +clone-on-ref-ptr = "warn" +unnecessary-to-owned = "warn" +cloned-instead-of-copied = "warn" +flat-map-option = "warn" +implicit-clone = "warn" +or-fun-call = "warn" + +# === Code Style === +use-self = "warn" +option-if-let-else = "warn" +uninlined-format-args = "warn" +manual-string-new = "warn" +single-char-pattern = "warn" +redundant-else = "warn" +match-same-arms = "warn" + +# === Safety & Documentation === +undocumented-unsafe-blocks = "warn" +doc-markdown = "warn" + +# === Debugging Artifacts === +dbg-macro = "warn" + +# === Code Quality === +branches-sharing-code = "warn" +derive-partial-eq-without-eq = "warn" +explicit-into-iter-loop = "warn" +explicit-iter-loop = "warn" +iter-with-drain = "warn" +needless-pass-by-ref-mut = "warn" +string-lit-as-bytes = "warn" [workspace.dependencies] -clap = { version = "4.0", features = ["derive", "env"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt", "ansi", "json"] } -tokio = { version = "1.0", features = ["full"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -metrics = "0.24.1" -metrics-derive = "0.1" -cadence = "1.4" +# Internal crates +tips-core = { path = "crates/tips/core" } +tips-audit-lib = { path = "crates/tips/audit" } + +# base-reth +base-reth-rpc-types = { git = "https://github.com/base/base", branch = "main" } +base-bundles = { git = "https://github.com/base/base", branch = "main" } + +# revm +op-revm = { version = "15.0.0", default-features = false } +revm-context-interface = { version = "15.0.0", default-features = false } # alloy -alloy-primitives = { version = "1.5.2", default-features = false, features = [ - "map-foldhash", -] } -alloy-genesis = { version = "1.5.2", default-features = false } -alloy-eips = { version = "1.5.2", default-features = false } -alloy-rpc-types = { version = "1.5.2", default-features = false } -alloy-rpc-types-engine = { version = "1.5.2", default-features = false } -alloy-rpc-types-eth = { version = "1.5.2" } -alloy-consensus = { version = "1.5.2" } +alloy-serde = { version = "1.0.41", default-features = false } +alloy-signer = { version = "1.0.41", default-features = false } +alloy-network = { version = "1.0.41", default-features = false } +alloy-provider = { version = "1.0.41", default-features = false } +alloy-consensus = { version = "1.0.41", default-features = false } +alloy-sol-types = { version = "1.4.1", default-features = false } +alloy-rpc-types = { version = "1.1.2", default-features = false } +alloy-primitives = { version = "1.4.1", default-features = false } +alloy-signer-local = { version = "1.0.41", default-features = false } +# alloy deps needed by existing services +alloy-genesis = { version = "1.0.41", default-features = false } +alloy-eips = { version = "1.0.41", default-features = false } +alloy-rpc-types-engine = { version = "1.0.41", default-features = false } +alloy-rpc-types-eth = { version = "1.0.41" } alloy-trie = { version = "0.9.1", default-features = false } -alloy-provider = { version = "1.5.2" } alloy-hardforks = { version = "0.5" } -alloy-rpc-client = { version = "1.5.2" } -alloy-transport-http = { version = "1.5.2" } +alloy-rpc-client = { version = "1.0.41" } +alloy-transport-http = { version = "1.0.41" } # op-alloy +op-alloy-flz = { version = "0.13.1", default-features = false } +op-alloy-network = { version = "0.22.0", default-features = false } op-alloy-rpc-types = { version = "0.22.0", default-features = false } -op-alloy-rpc-types-engine = { version = "0.22.0", default-features = false } op-alloy-consensus = { version = "0.22.0", default-features = false } +op-alloy-rpc-types-engine = { version = "0.22.0", default-features = false } + +# tokio +tokio = { version = "1.47.1", default-features = false } + +# async +async-trait = "0.1.89" + +# rpc +jsonrpsee = { version = "0.26.0", default-features = false } + +# kafka and s3 +bytes = { version = "1.8.0", default-features = false } +rdkafka = { version = "0.37.0", default-features = false } +aws-config = { version = "1.1.7", default-features = false } +aws-sdk-s3 = { version = "1.106.0", default-features = false } +aws-credential-types = { version = "1.1.7", default-features = false } + +# misc +url = { version = "2.5.7", default-features = false } +axum = { version = "0.8.3", default-features = false } +serde = { version = "1.0.219", default-features = false } +uuid = { version = "1.18.1", default-features = false } +clap = { version = "4.5.47", default-features = false, features = ["std", "derive", "env"] } +backon = { version = "1.5.2", default-features = false } +chrono = { version = "0.4.42", default-features = false } +anyhow = { version = "1.0.99", default-features = false } +tracing = { version = "0.1.41", default-features = false } +wiremock = { version = "0.6.2", default-features = false } +dotenvy = { version = "0.15.7", default-features = false } +metrics = { version = "0.24.1", default-features = false } +metrics-derive = { version = "0.1", default-features = false } +serde_json = { version = "1.0.143", default-features = false } +testcontainers = { version = "0.23.1", default-features = false } +tracing-subscriber = { version = "0.3.20", default-features = false } +testcontainers-modules = { version = "0.11.2", default-features = false } +metrics-exporter-prometheus = { version = "0.17.0", default-features = false } +futures = { version = "0.3.31", default-features = false } +moka = { version = "0.12.12", default-features = false } + +# existing service dependencies +cadence = "1.4" diff --git a/bin/tips/audit/Cargo.toml b/bin/tips/audit/Cargo.toml new file mode 100644 index 0000000..7d43244 --- /dev/null +++ b/bin/tips/audit/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tips-audit" +version.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +edition.workspace = true + +[dependencies] +tips-core.workspace = true +tips-audit-lib.workspace = true +clap.workspace = true +tokio.workspace = true +anyhow.workspace = true +tracing.workspace = true +dotenvy.workspace = true +rdkafka.workspace = true +aws-config.workspace = true +aws-sdk-s3.workspace = true +aws-credential-types.workspace = true diff --git a/bin/tips/audit/src/main.rs b/bin/tips/audit/src/main.rs new file mode 100644 index 0000000..4f031a4 --- /dev/null +++ b/bin/tips/audit/src/main.rs @@ -0,0 +1,137 @@ +use std::net::SocketAddr; + +use anyhow::Result; +use aws_config::{BehaviorVersion, Region}; +use aws_credential_types::Credentials; +use aws_sdk_s3::{Client as S3Client, config::Builder as S3ConfigBuilder}; +use clap::{Parser, ValueEnum}; +use rdkafka::consumer::Consumer; +use tips_audit_lib::{ + KafkaAuditArchiver, KafkaAuditLogReader, S3EventReaderWriter, create_kafka_consumer, +}; +use tips_core::{logger::init_logger_with_format, metrics::init_prometheus_exporter}; +use tracing::info; + +#[derive(Debug, Clone, ValueEnum)] +enum S3ConfigType { + Aws, + Manual, +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + #[arg(long, env = "TIPS_AUDIT_KAFKA_PROPERTIES_FILE")] + kafka_properties_file: String, + + #[arg(long, env = "TIPS_AUDIT_KAFKA_TOPIC")] + kafka_topic: String, + + #[arg(long, env = "TIPS_AUDIT_S3_BUCKET")] + s3_bucket: String, + + #[arg(long, env = "TIPS_AUDIT_LOG_LEVEL", default_value = "info")] + log_level: String, + + #[arg(long, env = "TIPS_AUDIT_LOG_FORMAT", default_value = "pretty")] + log_format: tips_core::logger::LogFormat, + + #[arg(long, env = "TIPS_AUDIT_S3_CONFIG_TYPE", default_value = "aws")] + s3_config_type: S3ConfigType, + + #[arg(long, env = "TIPS_AUDIT_S3_ENDPOINT")] + s3_endpoint: Option, + + #[arg(long, env = "TIPS_AUDIT_S3_REGION", default_value = "us-east-1")] + s3_region: String, + + #[arg(long, env = "TIPS_AUDIT_S3_ACCESS_KEY_ID")] + s3_access_key_id: Option, + + #[arg(long, env = "TIPS_AUDIT_S3_SECRET_ACCESS_KEY")] + s3_secret_access_key: Option, + + #[arg(long, env = "TIPS_AUDIT_METRICS_ADDR", default_value = "0.0.0.0:9002")] + metrics_addr: SocketAddr, + + #[arg(long, env = "TIPS_AUDIT_WORKER_POOL_SIZE", default_value = "80")] + worker_pool_size: usize, + + #[arg(long, env = "TIPS_AUDIT_CHANNEL_BUFFER_SIZE", default_value = "500")] + channel_buffer_size: usize, + + #[arg(long, env = "TIPS_AUDIT_NOOP_ARCHIVE", default_value = "false")] + noop_archive: bool, +} + +#[tokio::main] +async fn main() -> Result<()> { + dotenvy::dotenv().ok(); + + let args = Args::parse(); + + init_logger_with_format(&args.log_level, args.log_format); + + init_prometheus_exporter(args.metrics_addr).expect("Failed to install Prometheus exporter"); + + info!( + kafka_properties_file = %args.kafka_properties_file, + kafka_topic = %args.kafka_topic, + s3_bucket = %args.s3_bucket, + metrics_addr = %args.metrics_addr, + "Starting audit archiver" + ); + + let consumer = create_kafka_consumer(&args.kafka_properties_file)?; + consumer.subscribe(&[&args.kafka_topic])?; + + let reader = KafkaAuditLogReader::new(consumer, args.kafka_topic.clone())?; + + let s3_client = create_s3_client(&args).await?; + let s3_bucket = args.s3_bucket.clone(); + let writer = S3EventReaderWriter::new(s3_client, s3_bucket); + + let mut archiver = KafkaAuditArchiver::new( + reader, + writer, + args.worker_pool_size, + args.channel_buffer_size, + args.noop_archive, + ); + + info!("Audit archiver initialized, starting main loop"); + + archiver.run().await +} + +async fn create_s3_client(args: &Args) -> Result { + match args.s3_config_type { + S3ConfigType::Manual => { + let region = args.s3_region.clone(); + let mut config_builder = + aws_config::defaults(BehaviorVersion::latest()).region(Region::new(region)); + + if let Some(endpoint) = &args.s3_endpoint { + config_builder = config_builder.endpoint_url(endpoint); + } + + if let (Some(access_key), Some(secret_key)) = + (&args.s3_access_key_id, &args.s3_secret_access_key) + { + let credentials = Credentials::new(access_key, secret_key, None, None, "manual"); + config_builder = config_builder.credentials_provider(credentials); + } + + let config = config_builder.load().await; + let s3_config_builder = S3ConfigBuilder::from(&config).force_path_style(true); + + info!(message = "manually configuring s3 client"); + Ok(S3Client::from_conf(s3_config_builder.build())) + } + S3ConfigType::Aws => { + info!(message = "using aws s3 client"); + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; + Ok(S3Client::new(&config)) + } + } +} diff --git a/crates/mempool-rebroadcaster/Cargo.toml b/crates/mempool-rebroadcaster/Cargo.toml index 9d7ff5e..10dd671 100644 --- a/crates/mempool-rebroadcaster/Cargo.toml +++ b/crates/mempool-rebroadcaster/Cargo.toml @@ -10,8 +10,8 @@ path = "src/main.rs" [dependencies] clap = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true } -tokio = { workspace = true } +tracing-subscriber = { workspace = true, features = ["fmt", "std", "json", "env-filter"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } serde = { workspace = true } serde_json = { workspace = true } dotenvy = "0.15.7" @@ -23,5 +23,5 @@ alloy-rpc-types = { workspace = true, features = ["txpool"] } alloy-rpc-types-eth.workspace = true alloy-consensus.workspace = true alloy-trie.workspace = true -alloy-provider = { workspace = true, features = ["txpool-api"] } +alloy-provider = { workspace = true, features = ["txpool-api", "reqwest"] } diff --git a/crates/sidecrush/Cargo.toml b/crates/sidecrush/Cargo.toml index 1087d56..4e073ff 100644 --- a/crates/sidecrush/Cargo.toml +++ b/crates/sidecrush/Cargo.toml @@ -9,15 +9,15 @@ path = "src/bin/sidecrush.rs" [dependencies] tracing = { workspace = true } -tracing-subscriber = { workspace = true } -tokio = { workspace = true } +tracing-subscriber = { workspace = true, features = ["fmt", "std", "json"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] } cadence = { workspace = true } async-trait = "0.1" anyhow = "1.0" clap = { workspace = true } # Ethereum client deps (will be used for header fetching) -alloy-provider = { workspace = true } +alloy-provider = { workspace = true, features = ["reqwest"] } alloy-transport-http = { workspace = true } alloy-rpc-types-eth = { workspace = true } alloy-consensus = { workspace = true } diff --git a/crates/tips/audit/Cargo.toml b/crates/tips/audit/Cargo.toml new file mode 100644 index 0000000..6606ca2 --- /dev/null +++ b/crates/tips/audit/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "tips-audit-lib" +version.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[dependencies] +bytes.workspace = true +metrics.workspace = true +async-trait.workspace = true +metrics-derive.workspace = true +tips-core = { workspace = true, features = ["test-utils"] } +serde = { workspace = true, features = ["std", "derive"] } +tokio = { workspace = true, features = ["full"] } +uuid = { workspace = true, features = ["v5", "serde"] } +tracing = { workspace = true, features = ["std"] } +anyhow = { workspace = true, features = ["std"] } +serde_json = { workspace = true, features = ["std"] } +rdkafka = { workspace = true, features = ["tokio", "libz", "zstd", "ssl-vendored"] } +alloy-consensus = { workspace = true, features = ["std"] } +alloy-primitives = { workspace = true, features = ["map-foldhash", "serde"] } +aws-sdk-s3 = { workspace = true, features = ["rustls", "default-https-client", "rt-tokio"] } +futures = { workspace = true } + +[dev-dependencies] +testcontainers = { workspace = true, features = ["blocking"] } +testcontainers-modules = { workspace = true, features = ["postgres", "kafka", "minio"] } +aws-config = { workspace = true, features = ["default-https-client", "rt-tokio"] } diff --git a/crates/tips/audit/src/archiver.rs b/crates/tips/audit/src/archiver.rs new file mode 100644 index 0000000..d622094 --- /dev/null +++ b/crates/tips/audit/src/archiver.rs @@ -0,0 +1,156 @@ +use std::{ + fmt, + marker::PhantomData, + sync::Arc, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + +use anyhow::Result; +use tokio::{ + sync::{Mutex, mpsc}, + time::sleep, +}; +use tracing::{error, info}; + +use crate::{ + metrics::Metrics, + reader::{Event, EventReader}, + storage::EventWriter, +}; + +/// Archives audit events from Kafka to S3 storage. +pub struct KafkaAuditArchiver +where + R: EventReader, + W: EventWriter + Clone + Send + 'static, +{ + reader: R, + event_tx: mpsc::Sender, + metrics: Metrics, + _phantom: PhantomData, +} + +impl fmt::Debug for KafkaAuditArchiver +where + R: EventReader, + W: EventWriter + Clone + Send + 'static, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KafkaAuditArchiver").finish_non_exhaustive() + } +} + +impl KafkaAuditArchiver +where + R: EventReader, + W: EventWriter + Clone + Send + 'static, +{ + /// Creates a new archiver with the given reader and writer. + pub fn new( + reader: R, + writer: W, + worker_pool_size: usize, + channel_buffer_size: usize, + noop_archive: bool, + ) -> Self { + let (event_tx, event_rx) = mpsc::channel(channel_buffer_size); + let metrics = Metrics::default(); + + Self::spawn_workers(writer, event_rx, metrics.clone(), worker_pool_size, noop_archive); + + Self { reader, event_tx, metrics, _phantom: PhantomData } + } + + fn spawn_workers( + writer: W, + event_rx: mpsc::Receiver, + metrics: Metrics, + worker_pool_size: usize, + noop_archive: bool, + ) { + let event_rx = Arc::new(Mutex::new(event_rx)); + + for worker_id in 0..worker_pool_size { + let writer = writer.clone(); + let metrics = metrics.clone(); + let event_rx = Arc::clone(&event_rx); + + tokio::spawn(async move { + loop { + let event = { + let mut rx = event_rx.lock().await; + rx.recv().await + }; + + match event { + Some(event) => { + let archive_start = Instant::now(); + // tmp: only use this to clear kafka consumer offset + // TODO: use debug! later + if noop_archive { + info!( + worker_id, + bundle_id = %event.event.bundle_id(), + tx_ids = ?event.event.transaction_ids(), + timestamp = event.timestamp, + "Noop archive - skipping event" + ); + metrics.events_processed.increment(1); + metrics.in_flight_archive_tasks.decrement(1.0); + continue; + } + if let Err(e) = writer.archive_event(event).await { + error!(worker_id, error = %e, "Failed to write event"); + } else { + metrics + .archive_event_duration + .record(archive_start.elapsed().as_secs_f64()); + metrics.events_processed.increment(1); + } + metrics.in_flight_archive_tasks.decrement(1.0); + } + None => { + info!(worker_id, "Worker stopped - channel closed"); + break; + } + } + } + }); + } + } + + /// Runs the archiver loop, reading events and writing them to storage. + pub async fn run(&mut self) -> Result<()> { + loop { + let read_start = Instant::now(); + match self.reader.read_event().await { + Ok(event) => { + self.metrics.kafka_read_duration.record(read_start.elapsed().as_secs_f64()); + + let now_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + let event_age_ms = now_ms.saturating_sub(event.timestamp); + self.metrics.event_age.record(event_age_ms as f64); + + self.metrics.in_flight_archive_tasks.increment(1.0); + if let Err(e) = self.event_tx.send(event).await { + error!(error = %e, "Failed to send event to worker pool"); + self.metrics.in_flight_archive_tasks.decrement(1.0); + } + + let commit_start = Instant::now(); + if let Err(e) = self.reader.commit().await { + error!(error = %e, "Failed to commit message"); + } + self.metrics.kafka_commit_duration.record(commit_start.elapsed().as_secs_f64()); + } + Err(e) => { + error!(error = %e, "Error reading events"); + sleep(Duration::from_secs(1)).await; + } + } + } + } +} diff --git a/crates/tips/audit/src/lib.rs b/crates/tips/audit/src/lib.rs new file mode 100644 index 0000000..fe64f9f --- /dev/null +++ b/crates/tips/audit/src/lib.rs @@ -0,0 +1,73 @@ +//! Audit library for tracking and archiving bundle and user operation events. +//! +//! This crate provides functionality for publishing events to Kafka, +//! archiving them to S3, and reading event history. + +#![doc(issue_tracker_base_url = "https://github.com/base/tips/issues/")] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +mod archiver; +pub use archiver::KafkaAuditArchiver; + +mod metrics; +pub use metrics::Metrics; + +mod publisher; +pub use publisher::{ + BundleEventPublisher, KafkaBundleEventPublisher, KafkaUserOpEventPublisher, + LoggingBundleEventPublisher, LoggingUserOpEventPublisher, UserOpEventPublisher, +}; + +mod reader; +pub use reader::{ + Event, EventReader, KafkaAuditLogReader, KafkaUserOpAuditLogReader, UserOpEventReader, + UserOpEventWrapper, assign_topic_partition, create_kafka_consumer, +}; + +mod storage; +pub use storage::{ + BundleEventS3Reader, BundleHistory, BundleHistoryEvent, EventWriter, S3EventReaderWriter, + S3Key, TransactionMetadata, UserOpEventS3Reader, UserOpEventWriter, UserOpHistory, + UserOpHistoryEvent, +}; + +mod types; +use tokio::sync::mpsc; +use tracing::error; +pub use types::{ + BundleEvent, BundleId, DropReason, Transaction, TransactionId, UserOpDropReason, UserOpEvent, + UserOpHash, +}; + +/// Connects a bundle event receiver to a publisher, spawning a task to forward events. +pub fn connect_audit_to_publisher

(event_rx: mpsc::UnboundedReceiver, publisher: P) +where + P: BundleEventPublisher + 'static, +{ + tokio::spawn(async move { + let mut event_rx = event_rx; + while let Some(event) = event_rx.recv().await { + if let Err(e) = publisher.publish(event).await { + error!(error = %e, "failed to publish bundle event"); + } + } + }); +} + +/// Connects a user operation event receiver to a publisher, spawning a task to forward events. +pub fn connect_userop_audit_to_publisher

( + event_rx: mpsc::UnboundedReceiver, + publisher: P, +) where + P: UserOpEventPublisher + 'static, +{ + tokio::spawn(async move { + let mut event_rx = event_rx; + while let Some(event) = event_rx.recv().await { + if let Err(e) = publisher.publish(event).await { + error!(error = %e, "Failed to publish user op event"); + } + } + }); +} diff --git a/crates/tips/audit/src/metrics.rs b/crates/tips/audit/src/metrics.rs new file mode 100644 index 0000000..257b879 --- /dev/null +++ b/crates/tips/audit/src/metrics.rs @@ -0,0 +1,55 @@ +use metrics::{Counter, Gauge, Histogram}; +use metrics_derive::Metrics; + +/// Metrics for audit operations including Kafka reads, S3 writes, and event processing. +#[derive(Metrics, Clone)] +#[metrics(scope = "tips_audit")] +pub struct Metrics { + /// Duration of `archive_event` operations. + #[metric(describe = "Duration of archive_event")] + pub archive_event_duration: Histogram, + + /// Age of event when processed (now - event timestamp). + #[metric(describe = "Age of event when processed (now - event timestamp)")] + pub event_age: Histogram, + + /// Duration of Kafka `read_event` operations. + #[metric(describe = "Duration of Kafka read_event")] + pub kafka_read_duration: Histogram, + + /// Duration of Kafka commit operations. + #[metric(describe = "Duration of Kafka commit")] + pub kafka_commit_duration: Histogram, + + /// Duration of `update_bundle_history` operations. + #[metric(describe = "Duration of update_bundle_history")] + pub update_bundle_history_duration: Histogram, + + /// Duration of updating all transaction indexes. + #[metric(describe = "Duration of update all transaction indexes")] + pub update_tx_indexes_duration: Histogram, + + /// Duration of S3 `get_object` operations. + #[metric(describe = "Duration of S3 get_object")] + pub s3_get_duration: Histogram, + + /// Duration of S3 `put_object` operations. + #[metric(describe = "Duration of S3 put_object")] + pub s3_put_duration: Histogram, + + /// Total events processed. + #[metric(describe = "Total events processed")] + pub events_processed: Counter, + + /// Total S3 writes skipped due to deduplication. + #[metric(describe = "Total S3 writes skipped due to dedup")] + pub s3_writes_skipped: Counter, + + /// Number of in-flight archive tasks. + #[metric(describe = "Number of in-flight archive tasks")] + pub in_flight_archive_tasks: Gauge, + + /// Number of failed archive tasks. + #[metric(describe = "Number of failed archive tasks")] + pub failed_archive_tasks: Counter, +} diff --git a/crates/tips/audit/src/publisher.rs b/crates/tips/audit/src/publisher.rs new file mode 100644 index 0000000..b845f14 --- /dev/null +++ b/crates/tips/audit/src/publisher.rs @@ -0,0 +1,228 @@ +use anyhow::Result; +use async_trait::async_trait; +use rdkafka::producer::{FutureProducer, FutureRecord}; +use tracing::{debug, error, info}; + +use crate::types::{BundleEvent, UserOpEvent}; + +/// Trait for publishing bundle events. +#[async_trait] +pub trait BundleEventPublisher: Send + Sync { + /// Publishes a single bundle event. + async fn publish(&self, event: BundleEvent) -> Result<()>; + + /// Publishes multiple bundle events. + async fn publish_all(&self, events: Vec) -> Result<()>; +} + +/// Publishes bundle events to Kafka. +#[derive(Clone)] +pub struct KafkaBundleEventPublisher { + producer: FutureProducer, + topic: String, +} + +impl std::fmt::Debug for KafkaBundleEventPublisher { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KafkaBundleEventPublisher") + .field("topic", &self.topic) + .finish_non_exhaustive() + } +} + +impl KafkaBundleEventPublisher { + /// Creates a new Kafka bundle event publisher. + pub const fn new(producer: FutureProducer, topic: String) -> Self { + Self { producer, topic } + } + + async fn send_event(&self, event: &BundleEvent) -> Result<()> { + let bundle_id = event.bundle_id(); + let key = event.generate_event_key(); + let payload = serde_json::to_vec(event)?; + + let record = FutureRecord::to(&self.topic).key(&key).payload(&payload); + + match self.producer.send(record, tokio::time::Duration::from_secs(5)).await { + Ok(_) => { + debug!( + bundle_id = %bundle_id, + topic = %self.topic, + payload_size = payload.len(), + "successfully published event" + ); + Ok(()) + } + Err((err, _)) => { + error!( + bundle_id = %bundle_id, + topic = %self.topic, + error = %err, + "failed to publish event" + ); + Err(anyhow::anyhow!("Failed to publish event: {err}")) + } + } + } +} + +#[async_trait] +impl BundleEventPublisher for KafkaBundleEventPublisher { + async fn publish(&self, event: BundleEvent) -> Result<()> { + self.send_event(&event).await + } + + async fn publish_all(&self, events: Vec) -> Result<()> { + for event in events { + self.send_event(&event).await?; + } + Ok(()) + } +} + +/// Publishes bundle events to logs (for testing/debugging). +#[derive(Clone, Debug)] +pub struct LoggingBundleEventPublisher; + +impl LoggingBundleEventPublisher { + /// Creates a new logging bundle event publisher. + pub const fn new() -> Self { + Self + } +} + +impl Default for LoggingBundleEventPublisher { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl BundleEventPublisher for LoggingBundleEventPublisher { + async fn publish(&self, event: BundleEvent) -> Result<()> { + info!( + bundle_id = %event.bundle_id(), + event = ?event, + "Received bundle event" + ); + Ok(()) + } + + async fn publish_all(&self, events: Vec) -> Result<()> { + for event in events { + self.publish(event).await?; + } + Ok(()) + } +} + +/// Trait for publishing user operation events. +#[async_trait] +pub trait UserOpEventPublisher: Send + Sync { + /// Publishes a single user operation event. + async fn publish(&self, event: UserOpEvent) -> Result<()>; + + /// Publishes multiple user operation events. + async fn publish_all(&self, events: Vec) -> Result<()>; +} + +/// Publishes user operation events to Kafka. +#[derive(Clone)] +pub struct KafkaUserOpEventPublisher { + producer: FutureProducer, + topic: String, +} + +impl std::fmt::Debug for KafkaUserOpEventPublisher { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KafkaUserOpEventPublisher") + .field("topic", &self.topic) + .finish_non_exhaustive() + } +} + +impl KafkaUserOpEventPublisher { + /// Creates a new Kafka user operation event publisher. + pub const fn new(producer: FutureProducer, topic: String) -> Self { + Self { producer, topic } + } + + async fn send_event(&self, event: &UserOpEvent) -> Result<()> { + let user_op_hash = event.user_op_hash(); + let key = event.generate_event_key(); + let payload = serde_json::to_vec(event)?; + + let record = FutureRecord::to(&self.topic).key(&key).payload(&payload); + + match self.producer.send(record, tokio::time::Duration::from_secs(5)).await { + Ok(_) => { + debug!( + user_op_hash = %user_op_hash, + topic = %self.topic, + payload_size = payload.len(), + "Successfully published user op event" + ); + Ok(()) + } + Err((err, _)) => { + error!( + user_op_hash = %user_op_hash, + topic = %self.topic, + error = %err, + "Failed to publish user op event" + ); + Err(anyhow::anyhow!("Failed to publish user op event: {err}")) + } + } + } +} + +#[async_trait] +impl UserOpEventPublisher for KafkaUserOpEventPublisher { + async fn publish(&self, event: UserOpEvent) -> Result<()> { + self.send_event(&event).await + } + + async fn publish_all(&self, events: Vec) -> Result<()> { + for event in events { + self.send_event(&event).await?; + } + Ok(()) + } +} + +/// Publishes user operation events to logs (for testing/debugging). +#[derive(Clone, Debug)] +pub struct LoggingUserOpEventPublisher; + +impl LoggingUserOpEventPublisher { + /// Creates a new logging user operation event publisher. + pub const fn new() -> Self { + Self + } +} + +impl Default for LoggingUserOpEventPublisher { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl UserOpEventPublisher for LoggingUserOpEventPublisher { + async fn publish(&self, event: UserOpEvent) -> Result<()> { + info!( + user_op_hash = %event.user_op_hash(), + event = ?event, + "Received user op event" + ); + Ok(()) + } + + async fn publish_all(&self, events: Vec) -> Result<()> { + for event in events { + self.publish(event).await?; + } + Ok(()) + } +} diff --git a/crates/tips/audit/src/reader.rs b/crates/tips/audit/src/reader.rs new file mode 100644 index 0000000..9df82a7 --- /dev/null +++ b/crates/tips/audit/src/reader.rs @@ -0,0 +1,246 @@ +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use anyhow::Result; +use async_trait::async_trait; +use rdkafka::{ + Timestamp, TopicPartitionList, + config::ClientConfig, + consumer::{Consumer, StreamConsumer}, + message::Message, +}; +use tips_core::kafka::load_kafka_config_from_file; +use tokio::time::sleep; +use tracing::{debug, error, info}; + +use crate::types::{BundleEvent, UserOpEvent}; + +/// Creates a Kafka consumer from a properties file. +pub fn create_kafka_consumer(kafka_properties_file: &str) -> Result { + let client_config: ClientConfig = + ClientConfig::from_iter(load_kafka_config_from_file(kafka_properties_file)?); + let consumer: StreamConsumer = client_config.create()?; + Ok(consumer) +} + +/// Assigns a topic partition to a consumer. +pub fn assign_topic_partition(consumer: &StreamConsumer, topic: &str) -> Result<()> { + let mut tpl = TopicPartitionList::new(); + tpl.add_partition(topic, 0); + consumer.assign(&tpl)?; + Ok(()) +} + +/// A bundle event with metadata from Kafka. +#[derive(Debug, Clone)] +pub struct Event { + /// The event key. + pub key: String, + /// The bundle event. + pub event: BundleEvent, + /// The event timestamp in milliseconds. + pub timestamp: i64, +} + +/// Trait for reading bundle events. +#[async_trait] +pub trait EventReader { + /// Reads the next event. + async fn read_event(&mut self) -> Result; + /// Commits the last read message. + async fn commit(&mut self) -> Result<()>; +} + +/// Reads bundle audit events from Kafka. +pub struct KafkaAuditLogReader { + consumer: StreamConsumer, + topic: String, + last_message_offset: Option, + last_message_partition: Option, +} + +impl std::fmt::Debug for KafkaAuditLogReader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KafkaAuditLogReader") + .field("topic", &self.topic) + .field("last_message_offset", &self.last_message_offset) + .field("last_message_partition", &self.last_message_partition) + .finish_non_exhaustive() + } +} + +impl KafkaAuditLogReader { + /// Creates a new Kafka audit log reader. + pub fn new(consumer: StreamConsumer, topic: String) -> Result { + consumer.subscribe(&[&topic])?; + Ok(Self { consumer, topic, last_message_offset: None, last_message_partition: None }) + } +} + +#[async_trait] +impl EventReader for KafkaAuditLogReader { + async fn read_event(&mut self) -> Result { + match self.consumer.recv().await { + Ok(message) => { + let payload = + message.payload().ok_or_else(|| anyhow::anyhow!("Message has no payload"))?; + + // Extract Kafka timestamp, use current time as fallback + let timestamp = match message.timestamp() { + Timestamp::CreateTime(millis) | Timestamp::LogAppendTime(millis) => millis, + Timestamp::NotAvailable => { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() + as i64 + } + }; + + let event: BundleEvent = serde_json::from_slice(payload)?; + + info!( + bundle_id = %event.bundle_id(), + tx_ids = ?event.transaction_ids(), + timestamp = timestamp, + offset = message.offset(), + partition = message.partition(), + "Received event with timestamp" + ); + + self.last_message_offset = Some(message.offset()); + self.last_message_partition = Some(message.partition()); + + let key = message + .key() + .map(|k| String::from_utf8_lossy(k).to_string()) + .ok_or_else(|| anyhow::anyhow!("Message missing required key"))?; + + let event_result = Event { key, event, timestamp }; + + Ok(event_result) + } + Err(e) => { + error!(error = %e, "Error receiving message from Kafka"); + sleep(Duration::from_secs(1)).await; + Err(e.into()) + } + } + } + + async fn commit(&mut self) -> Result<()> { + if let (Some(offset), Some(partition)) = + (self.last_message_offset, self.last_message_partition) + { + let mut tpl = TopicPartitionList::new(); + tpl.add_partition_offset(&self.topic, partition, rdkafka::Offset::Offset(offset + 1))?; + self.consumer.commit(&tpl, rdkafka::consumer::CommitMode::Async)?; + } + Ok(()) + } +} + +impl KafkaAuditLogReader { + /// Returns the topic this reader is subscribed to. + pub fn topic(&self) -> &str { + &self.topic + } +} + +/// A user operation event with metadata from Kafka. +#[derive(Debug, Clone)] +pub struct UserOpEventWrapper { + /// The event key. + pub key: String, + /// The user operation event. + pub event: UserOpEvent, + /// The event timestamp in milliseconds. + pub timestamp: i64, +} + +/// Trait for reading user operation events. +#[async_trait] +pub trait UserOpEventReader { + /// Reads the next user operation event. + async fn read_event(&mut self) -> Result; + /// Commits the last read message. + async fn commit(&mut self) -> Result<()>; +} + +/// Reads user operation audit events from Kafka. +pub struct KafkaUserOpAuditLogReader { + consumer: StreamConsumer, + topic: String, + last_message_offset: Option, + last_message_partition: Option, +} + +impl std::fmt::Debug for KafkaUserOpAuditLogReader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("KafkaUserOpAuditLogReader") + .field("topic", &self.topic) + .field("last_message_offset", &self.last_message_offset) + .field("last_message_partition", &self.last_message_partition) + .finish_non_exhaustive() + } +} + +impl KafkaUserOpAuditLogReader { + /// Creates a new Kafka user operation audit log reader. + pub fn new(consumer: StreamConsumer, topic: String) -> Result { + consumer.subscribe(&[&topic])?; + Ok(Self { consumer, topic, last_message_offset: None, last_message_partition: None }) + } +} + +#[async_trait] +impl UserOpEventReader for KafkaUserOpAuditLogReader { + async fn read_event(&mut self) -> Result { + match self.consumer.recv().await { + Ok(message) => { + let payload = + message.payload().ok_or_else(|| anyhow::anyhow!("Message has no payload"))?; + + let timestamp = match message.timestamp() { + Timestamp::CreateTime(millis) | Timestamp::LogAppendTime(millis) => millis, + Timestamp::NotAvailable => { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() + as i64 + } + }; + + let event: UserOpEvent = serde_json::from_slice(payload)?; + + debug!( + user_op_hash = %event.user_op_hash(), + timestamp = timestamp, + offset = message.offset(), + partition = message.partition(), + "Received UserOp event" + ); + + self.last_message_offset = Some(message.offset()); + self.last_message_partition = Some(message.partition()); + + let key = message + .key() + .map(|k| String::from_utf8_lossy(k).to_string()) + .ok_or_else(|| anyhow::anyhow!("Message missing required key"))?; + + Ok(UserOpEventWrapper { key, event, timestamp }) + } + Err(e) => { + error!(error = %e, "Error receiving UserOp message from Kafka"); + sleep(Duration::from_secs(1)).await; + Err(e.into()) + } + } + } + + async fn commit(&mut self) -> Result<()> { + if let (Some(offset), Some(partition)) = + (self.last_message_offset, self.last_message_partition) + { + let mut tpl = TopicPartitionList::new(); + tpl.add_partition_offset(&self.topic, partition, rdkafka::Offset::Offset(offset + 1))?; + self.consumer.commit(&tpl, rdkafka::consumer::CommitMode::Async)?; + } + Ok(()) + } +} diff --git a/crates/tips/audit/src/storage.rs b/crates/tips/audit/src/storage.rs new file mode 100644 index 0000000..52281cf --- /dev/null +++ b/crates/tips/audit/src/storage.rs @@ -0,0 +1,924 @@ +use std::{fmt, fmt::Debug, time::Instant}; + +use alloy_primitives::{Address, TxHash, U256}; +use anyhow::Result; +use async_trait::async_trait; +use aws_sdk_s3::{ + Client as S3Client, error::SdkError, operation::get_object::GetObjectError, + primitives::ByteStream, +}; +use futures::future; +use serde::{Deserialize, Serialize}; +use tips_core::AcceptedBundle; +use tracing::info; + +use crate::{ + metrics::Metrics, + reader::Event, + types::{ + BundleEvent, BundleId, DropReason, TransactionId, UserOpDropReason, UserOpEvent, UserOpHash, + }, +}; + +/// S3 key types for storing different event types. +#[derive(Debug)] +pub enum S3Key { + /// Key for bundle events. + Bundle(BundleId), + /// Key for transaction lookups by hash. + TransactionByHash(TxHash), + /// Key for user operation events. + UserOp(UserOpHash), +} + +impl fmt::Display for S3Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Bundle(bundle_id) => write!(f, "bundles/{bundle_id}"), + Self::TransactionByHash(hash) => write!(f, "transactions/by_hash/{hash}"), + Self::UserOp(user_op_hash) => write!(f, "userops/{user_op_hash}"), + } + } +} + +/// Metadata for a transaction, tracking which bundles it belongs to. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct TransactionMetadata { + /// Bundle IDs that contain this transaction. + pub bundle_ids: Vec, +} + +/// History event for a bundle. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "event", content = "data")] +pub enum BundleHistoryEvent { + /// Bundle was received. + Received { + /// Event key. + key: String, + /// Event timestamp. + timestamp: i64, + /// The accepted bundle. + bundle: Box, + }, + /// Bundle was cancelled. + Cancelled { + /// Event key. + key: String, + /// Event timestamp. + timestamp: i64, + }, + /// Bundle was included by a builder. + BuilderIncluded { + /// Event key. + key: String, + /// Event timestamp. + timestamp: i64, + /// Builder identifier. + builder: String, + /// Block number. + block_number: u64, + /// Flashblock index. + flashblock_index: u64, + }, + /// Bundle was included in a block. + BlockIncluded { + /// Event key. + key: String, + /// Event timestamp. + timestamp: i64, + /// Block number. + block_number: u64, + /// Block hash. + block_hash: TxHash, + }, + /// Bundle was dropped. + Dropped { + /// Event key. + key: String, + /// Event timestamp. + timestamp: i64, + /// Drop reason. + reason: DropReason, + }, +} + +impl BundleHistoryEvent { + /// Returns the event key. + pub fn key(&self) -> &str { + match self { + Self::Received { key, .. } + | Self::Cancelled { key, .. } + | Self::BuilderIncluded { key, .. } + | Self::BlockIncluded { key, .. } + | Self::Dropped { key, .. } => key, + } + } +} + +/// History of events for a bundle. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct BundleHistory { + /// List of history events. + pub history: Vec, +} + +/// History event for a user operation. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "event", content = "data")] +pub enum UserOpHistoryEvent { + /// User operation was added to the mempool. + AddedToMempool { + /// Event key. + key: String, + /// Event timestamp. + timestamp: i64, + /// Sender address. + sender: Address, + /// Entry point address. + entry_point: Address, + /// Nonce. + nonce: U256, + }, + /// User operation was dropped. + Dropped { + /// Event key. + key: String, + /// Event timestamp. + timestamp: i64, + /// Drop reason. + reason: UserOpDropReason, + }, + /// User operation was included in a block. + Included { + /// Event key. + key: String, + /// Event timestamp. + timestamp: i64, + /// Block number. + block_number: u64, + /// Transaction hash. + tx_hash: TxHash, + }, +} + +impl UserOpHistoryEvent { + /// Returns the event key. + pub fn key(&self) -> &str { + match self { + Self::AddedToMempool { key, .. } + | Self::Dropped { key, .. } + | Self::Included { key, .. } => key, + } + } +} + +/// History of events for a user operation. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct UserOpHistory { + /// List of history events. + pub history: Vec, +} + +pub(crate) use crate::reader::UserOpEventWrapper; + +fn update_bundle_history_transform( + bundle_history: BundleHistory, + event: &Event, +) -> Option { + let mut history = bundle_history.history; + let bundle_id = event.event.bundle_id(); + + // Check for deduplication - if event with same key already exists, skip + if history.iter().any(|h| h.key() == event.key) { + info!( + bundle_id = %bundle_id, + event_key = %event.key, + "Event already exists, skipping due to deduplication" + ); + return None; + } + + let history_event = match &event.event { + BundleEvent::Received { bundle, .. } => BundleHistoryEvent::Received { + key: event.key.clone(), + timestamp: event.timestamp, + bundle: bundle.clone(), + }, + BundleEvent::Cancelled { .. } => { + BundleHistoryEvent::Cancelled { key: event.key.clone(), timestamp: event.timestamp } + } + BundleEvent::BuilderIncluded { builder, block_number, flashblock_index, .. } => { + BundleHistoryEvent::BuilderIncluded { + key: event.key.clone(), + timestamp: event.timestamp, + builder: builder.clone(), + block_number: *block_number, + flashblock_index: *flashblock_index, + } + } + BundleEvent::BlockIncluded { block_number, block_hash, .. } => { + BundleHistoryEvent::BlockIncluded { + key: event.key.clone(), + timestamp: event.timestamp, + block_number: *block_number, + block_hash: *block_hash, + } + } + BundleEvent::Dropped { reason, .. } => BundleHistoryEvent::Dropped { + key: event.key.clone(), + timestamp: event.timestamp, + reason: reason.clone(), + }, + }; + + history.push(history_event); + let bundle_history = BundleHistory { history }; + + info!( + bundle_id = %bundle_id, + event_count = bundle_history.history.len(), + "Updated bundle history" + ); + + Some(bundle_history) +} + +fn update_transaction_metadata_transform( + transaction_metadata: TransactionMetadata, + bundle_id: BundleId, +) -> Option { + let mut bundle_ids = transaction_metadata.bundle_ids; + + if bundle_ids.contains(&bundle_id) { + return None; + } + + bundle_ids.push(bundle_id); + Some(TransactionMetadata { bundle_ids }) +} + +fn update_userop_history_transform( + userop_history: UserOpHistory, + event: &UserOpEventWrapper, +) -> Option { + let mut history = userop_history.history; + let user_op_hash = event.event.user_op_hash(); + + if history.iter().any(|h| h.key() == event.key) { + info!( + user_op_hash = %user_op_hash, + event_key = %event.key, + "UserOp event already exists, skipping due to deduplication" + ); + return None; + } + + let history_event = match &event.event { + UserOpEvent::AddedToMempool { sender, entry_point, nonce, .. } => { + UserOpHistoryEvent::AddedToMempool { + key: event.key.clone(), + timestamp: event.timestamp, + sender: *sender, + entry_point: *entry_point, + nonce: *nonce, + } + } + UserOpEvent::Dropped { reason, .. } => UserOpHistoryEvent::Dropped { + key: event.key.clone(), + timestamp: event.timestamp, + reason: reason.clone(), + }, + UserOpEvent::Included { block_number, tx_hash, .. } => UserOpHistoryEvent::Included { + key: event.key.clone(), + timestamp: event.timestamp, + block_number: *block_number, + tx_hash: *tx_hash, + }, + }; + + history.push(history_event); + let userop_history = UserOpHistory { history }; + + info!( + user_op_hash = %user_op_hash, + event_count = userop_history.history.len(), + "Updated user op history" + ); + + Some(userop_history) +} + +/// Trait for writing bundle events to storage. +#[async_trait] +pub trait EventWriter { + /// Archives a bundle event. + async fn archive_event(&self, event: Event) -> Result<()>; +} + +/// Trait for writing user operation events to storage. +#[async_trait] +pub trait UserOpEventWriter { + /// Archives a user operation event. + async fn archive_userop_event(&self, event: UserOpEventWrapper) -> Result<()>; +} + +/// Trait for reading bundle events from S3. +#[async_trait] +pub trait BundleEventS3Reader { + /// Gets the bundle history for a given bundle ID. + async fn get_bundle_history(&self, bundle_id: BundleId) -> Result>; + /// Gets transaction metadata for a given transaction hash. + async fn get_transaction_metadata( + &self, + tx_hash: TxHash, + ) -> Result>; +} + +/// Trait for reading user operation events from S3. +#[async_trait] +pub trait UserOpEventS3Reader { + /// Gets the user operation history for a given hash. + async fn get_userop_history(&self, user_op_hash: UserOpHash) -> Result>; +} + +/// S3-backed event reader and writer. +#[derive(Clone, Debug)] +pub struct S3EventReaderWriter { + s3_client: S3Client, + bucket: String, + metrics: Metrics, +} + +impl S3EventReaderWriter { + /// Creates a new S3 event reader/writer. + pub fn new(s3_client: S3Client, bucket: String) -> Self { + Self { s3_client, bucket, metrics: Metrics::default() } + } + + async fn update_bundle_history(&self, event: Event) -> Result<()> { + let s3_key = S3Key::Bundle(event.event.bundle_id()).to_string(); + + self.idempotent_write::(&s3_key, |current_history| { + update_bundle_history_transform(current_history, &event) + }) + .await + } + + async fn update_transaction_by_hash_index( + &self, + tx_id: &TransactionId, + bundle_id: BundleId, + ) -> Result<()> { + let s3_key = S3Key::TransactionByHash(tx_id.hash); + let key = s3_key.to_string(); + + self.idempotent_write::(&key, |current_metadata| { + update_transaction_metadata_transform(current_metadata, bundle_id) + }) + .await + } + + async fn update_userop_history(&self, event: UserOpEventWrapper) -> Result<()> { + let s3_key = S3Key::UserOp(event.event.user_op_hash()).to_string(); + + self.idempotent_write::(&s3_key, |current_history| { + update_userop_history_transform(current_history, &event) + }) + .await + } + + async fn idempotent_write(&self, key: &str, mut transform_fn: F) -> Result<()> + where + T: for<'de> Deserialize<'de> + Serialize + Clone + Default + Debug, + F: FnMut(T) -> Option, + { + const MAX_RETRIES: usize = 5; + const BASE_DELAY_MS: u64 = 100; + + for attempt in 0..MAX_RETRIES { + let get_start = Instant::now(); + let (current_value, etag) = self.get_object_with_etag::(key).await?; + self.metrics.s3_get_duration.record(get_start.elapsed().as_secs_f64()); + + let value = current_value.unwrap_or_default(); + + match transform_fn(value.clone()) { + Some(new_value) => { + let content = serde_json::to_string(&new_value)?; + + let mut put_request = self + .s3_client + .put_object() + .bucket(&self.bucket) + .key(key) + .body(ByteStream::from(content.into_bytes())); + + if let Some(etag) = etag { + put_request = put_request.if_match(etag); + } else { + put_request = put_request.if_none_match("*"); + } + + let put_start = Instant::now(); + match put_request.send().await { + Ok(_) => { + self.metrics.s3_put_duration.record(put_start.elapsed().as_secs_f64()); + info!( + s3_key = %key, + attempt = attempt + 1, + "Successfully wrote object with idempotent write" + ); + return Ok(()); + } + Err(e) => { + self.metrics.s3_put_duration.record(put_start.elapsed().as_secs_f64()); + + if attempt < MAX_RETRIES - 1 { + let delay = BASE_DELAY_MS * 2_u64.pow(attempt as u32); + info!( + s3_key = %key, + attempt = attempt + 1, + delay_ms = delay, + error = %e, + "Conflict detected, retrying with backoff" + ); + tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await; + } else { + return Err(anyhow::anyhow!( + "Failed to write after {MAX_RETRIES} attempts: {e}" + )); + } + } + } + } + None => { + self.metrics.s3_writes_skipped.increment(1); + info!( + s3_key = %key, + "Transform function returned None, no write required" + ); + return Ok(()); + } + } + } + + Err(anyhow::anyhow!("Exceeded maximum retry attempts")) + } + + async fn get_object_with_etag(&self, key: &str) -> Result<(Option, Option)> + where + T: for<'de> Deserialize<'de>, + { + match self.s3_client.get_object().bucket(&self.bucket).key(key).send().await { + Ok(response) => { + let etag = response.e_tag().map(|s| s.to_string()); + let body = response.body.collect().await?; + let content = String::from_utf8(body.into_bytes().to_vec())?; + let value: T = serde_json::from_str(&content)?; + Ok((Some(value), etag)) + } + Err(e) => match &e { + SdkError::ServiceError(service_err) => match service_err.err() { + GetObjectError::NoSuchKey(_) => Ok((None, None)), + _ => Err(anyhow::anyhow!("Failed to get object: {e}")), + }, + _ => { + let error_string = e.to_string(); + if error_string.contains("NoSuchKey") + || error_string.contains("NotFound") + || error_string.contains("404") + { + Ok((None, None)) + } else { + Err(anyhow::anyhow!("Failed to get object: {e}")) + } + } + }, + } + } +} + +#[async_trait] +impl EventWriter for S3EventReaderWriter { + async fn archive_event(&self, event: Event) -> Result<()> { + let bundle_id = event.event.bundle_id(); + let transaction_ids = event.event.transaction_ids(); + + let bundle_start = Instant::now(); + let bundle_future = self.update_bundle_history(event); + + let tx_start = Instant::now(); + let tx_futures: Vec<_> = + transaction_ids + .into_iter() + .map(|tx_id| async move { + self.update_transaction_by_hash_index(&tx_id, bundle_id).await + }) + .collect(); + + // Run the bundle and transaction futures concurrently and wait for them to complete + tokio::try_join!(bundle_future, future::try_join_all(tx_futures))?; + + self.metrics.update_bundle_history_duration.record(bundle_start.elapsed().as_secs_f64()); + self.metrics.update_tx_indexes_duration.record(tx_start.elapsed().as_secs_f64()); + + Ok(()) + } +} + +#[async_trait] +impl BundleEventS3Reader for S3EventReaderWriter { + async fn get_bundle_history(&self, bundle_id: BundleId) -> Result> { + let s3_key = S3Key::Bundle(bundle_id).to_string(); + let (bundle_history, _) = self.get_object_with_etag::(&s3_key).await?; + Ok(bundle_history) + } + + async fn get_transaction_metadata( + &self, + tx_hash: TxHash, + ) -> Result> { + let s3_key = S3Key::TransactionByHash(tx_hash).to_string(); + let (transaction_metadata, _) = + self.get_object_with_etag::(&s3_key).await?; + Ok(transaction_metadata) + } +} + +#[async_trait] +impl UserOpEventWriter for S3EventReaderWriter { + async fn archive_userop_event(&self, event: UserOpEventWrapper) -> Result<()> { + self.update_userop_history(event).await + } +} + +#[async_trait] +impl UserOpEventS3Reader for S3EventReaderWriter { + async fn get_userop_history(&self, user_op_hash: UserOpHash) -> Result> { + let s3_key = S3Key::UserOp(user_op_hash).to_string(); + let (userop_history, _) = self.get_object_with_etag::(&s3_key).await?; + Ok(userop_history) + } +} + +#[cfg(test)] +mod tests { + use alloy_primitives::{Address, B256, TxHash, U256}; + use tips_core::{BundleExtensions, test_utils::create_bundle_from_txn_data}; + use uuid::Uuid; + + use super::*; + use crate::{ + reader::Event, + types::{BundleEvent, DropReason, UserOpDropReason, UserOpEvent}, + }; + + fn create_test_event(key: &str, timestamp: i64, bundle_event: BundleEvent) -> Event { + Event { key: key.to_string(), timestamp, event: bundle_event } + } + + #[test] + fn test_update_bundle_history_transform_adds_new_event() { + let bundle_history = BundleHistory { history: vec![] }; + let bundle = create_bundle_from_txn_data(); + let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + let bundle_event = BundleEvent::Received { bundle_id, bundle: Box::new(bundle.clone()) }; + let event = create_test_event("test-key", 1234567890, bundle_event); + + let result = update_bundle_history_transform(bundle_history, &event); + + assert!(result.is_some()); + let bundle_history = result.unwrap(); + assert_eq!(bundle_history.history.len(), 1); + + match &bundle_history.history[0] { + BundleHistoryEvent::Received { key, timestamp: ts, bundle: b } => { + assert_eq!(key, "test-key"); + assert_eq!(*ts, 1234567890); + assert_eq!(b.block_number, bundle.block_number); + } + _ => panic!("Expected Created event"), + } + } + + #[test] + fn test_update_bundle_history_transform_skips_duplicate_key() { + let existing_event = BundleHistoryEvent::Received { + key: "duplicate-key".to_string(), + timestamp: 1111111111, + bundle: Box::new(create_bundle_from_txn_data()), + }; + let bundle_history = BundleHistory { history: vec![existing_event] }; + + let bundle = create_bundle_from_txn_data(); + let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + let bundle_event = BundleEvent::Received { bundle_id, bundle: Box::new(bundle) }; + let event = create_test_event("duplicate-key", 1234567890, bundle_event); + + let result = update_bundle_history_transform(bundle_history, &event); + + assert!(result.is_none()); + } + + #[test] + fn test_update_bundle_history_transform_handles_all_event_types() { + let bundle_history = BundleHistory { history: vec![] }; + let bundle = create_bundle_from_txn_data(); + let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + + let bundle_event = BundleEvent::Received { bundle_id, bundle: Box::new(bundle) }; + let event = create_test_event("test-key", 1234567890, bundle_event); + let result = update_bundle_history_transform(bundle_history.clone(), &event); + assert!(result.is_some()); + + let bundle_event = BundleEvent::Cancelled { bundle_id }; + let event = create_test_event("test-key-2", 1234567890, bundle_event); + let result = update_bundle_history_transform(bundle_history.clone(), &event); + assert!(result.is_some()); + + let bundle_event = BundleEvent::BuilderIncluded { + bundle_id, + builder: "test-builder".to_string(), + block_number: 12345, + flashblock_index: 1, + }; + let event = create_test_event("test-key-3", 1234567890, bundle_event); + let result = update_bundle_history_transform(bundle_history.clone(), &event); + assert!(result.is_some()); + + let bundle_event = BundleEvent::BlockIncluded { + bundle_id, + block_number: 12345, + block_hash: TxHash::from([1u8; 32]), + }; + let event = create_test_event("test-key-4", 1234567890, bundle_event); + let result = update_bundle_history_transform(bundle_history.clone(), &event); + assert!(result.is_some()); + + let bundle_event = BundleEvent::Dropped { bundle_id, reason: DropReason::TimedOut }; + let event = create_test_event("test-key-5", 1234567890, bundle_event); + let result = update_bundle_history_transform(bundle_history, &event); + assert!(result.is_some()); + } + + #[test] + fn test_update_transaction_metadata_transform_adds_new_bundle() { + let metadata = TransactionMetadata { bundle_ids: vec![] }; + let bundle = create_bundle_from_txn_data(); + let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + + let result = update_transaction_metadata_transform(metadata, bundle_id); + + assert!(result.is_some()); + let metadata = result.unwrap(); + assert_eq!(metadata.bundle_ids.len(), 1); + assert_eq!(metadata.bundle_ids[0], bundle_id); + } + + #[test] + fn test_update_transaction_metadata_transform_skips_existing_bundle() { + let bundle = create_bundle_from_txn_data(); + let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + let metadata = TransactionMetadata { bundle_ids: vec![bundle_id] }; + + let result = update_transaction_metadata_transform(metadata, bundle_id); + + assert!(result.is_none()); + } + + #[test] + fn test_update_transaction_metadata_transform_adds_to_existing_bundles() { + // Some different, dummy bundle IDs since create_bundle_from_txn_data() returns the same bundle ID + // Even if the same txn is contained across multiple bundles, the bundle ID will be different since the + // UUID is based on the bundle hash. + let existing_bundle_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(); + let new_bundle_id = Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap(); + + let metadata = TransactionMetadata { bundle_ids: vec![existing_bundle_id] }; + + let result = update_transaction_metadata_transform(metadata, new_bundle_id); + + assert!(result.is_some()); + let metadata = result.unwrap(); + assert_eq!(metadata.bundle_ids.len(), 2); + assert!(metadata.bundle_ids.contains(&existing_bundle_id)); + assert!(metadata.bundle_ids.contains(&new_bundle_id)); + } + + fn create_test_userop_event( + key: &str, + timestamp: i64, + userop_event: UserOpEvent, + ) -> UserOpEventWrapper { + UserOpEventWrapper { key: key.to_string(), timestamp, event: userop_event } + } + + #[test] + fn test_s3_key_userop_display() { + let hash = B256::from([1u8; 32]); + let key = S3Key::UserOp(hash); + let key_str = key.to_string(); + assert!(key_str.starts_with("userops/")); + assert!(key_str.contains(&format!("{hash}"))); + } + + #[test] + fn test_update_userop_history_transform_adds_new_event() { + let userop_history = UserOpHistory { history: vec![] }; + let user_op_hash = B256::from([1u8; 32]); + let sender = Address::from([2u8; 20]); + let entry_point = Address::from([3u8; 20]); + let nonce = U256::from(1); + + let userop_event = UserOpEvent::AddedToMempool { user_op_hash, sender, entry_point, nonce }; + let event = create_test_userop_event("test-key", 1234567890, userop_event); + + let result = update_userop_history_transform(userop_history, &event); + + assert!(result.is_some()); + let history = result.unwrap(); + assert_eq!(history.history.len(), 1); + + match &history.history[0] { + UserOpHistoryEvent::AddedToMempool { + key, + timestamp: ts, + sender: s, + entry_point: ep, + nonce: n, + } => { + assert_eq!(key, "test-key"); + assert_eq!(*ts, 1234567890); + assert_eq!(*s, sender); + assert_eq!(*ep, entry_point); + assert_eq!(*n, nonce); + } + _ => panic!("Expected AddedToMempool event"), + } + } + + #[test] + fn test_update_userop_history_transform_skips_duplicate_key() { + let user_op_hash = B256::from([1u8; 32]); + let sender = Address::from([2u8; 20]); + let entry_point = Address::from([3u8; 20]); + let nonce = U256::from(1); + + let existing_event = UserOpHistoryEvent::AddedToMempool { + key: "duplicate-key".to_string(), + timestamp: 1111111111, + sender, + entry_point, + nonce, + }; + let userop_history = UserOpHistory { history: vec![existing_event] }; + + let userop_event = UserOpEvent::AddedToMempool { user_op_hash, sender, entry_point, nonce }; + let event = create_test_userop_event("duplicate-key", 1234567890, userop_event); + + let result = update_userop_history_transform(userop_history, &event); + + assert!(result.is_none()); + } + + #[test] + fn test_update_userop_history_transform_handles_dropped_event() { + let userop_history = UserOpHistory { history: vec![] }; + let user_op_hash = B256::from([1u8; 32]); + let reason = UserOpDropReason::Expired; + + let userop_event = UserOpEvent::Dropped { user_op_hash, reason }; + let event = create_test_userop_event("dropped-key", 1234567890, userop_event); + + let result = update_userop_history_transform(userop_history, &event); + + assert!(result.is_some()); + let history = result.unwrap(); + assert_eq!(history.history.len(), 1); + + match &history.history[0] { + UserOpHistoryEvent::Dropped { key, timestamp, reason: r } => { + assert_eq!(key, "dropped-key"); + assert_eq!(*timestamp, 1234567890); + match r { + UserOpDropReason::Expired => {} + _ => panic!("Expected Expired reason"), + } + } + _ => panic!("Expected Dropped event"), + } + } + + #[test] + fn test_update_userop_history_transform_handles_included_event() { + let userop_history = UserOpHistory { history: vec![] }; + let user_op_hash = B256::from([1u8; 32]); + let tx_hash = TxHash::from([4u8; 32]); + let block_number = 12345u64; + + let userop_event = UserOpEvent::Included { user_op_hash, block_number, tx_hash }; + let event = create_test_userop_event("included-key", 1234567890, userop_event); + + let result = update_userop_history_transform(userop_history, &event); + + assert!(result.is_some()); + let history = result.unwrap(); + assert_eq!(history.history.len(), 1); + + match &history.history[0] { + UserOpHistoryEvent::Included { key, timestamp, block_number: bn, tx_hash: th } => { + assert_eq!(key, "included-key"); + assert_eq!(*timestamp, 1234567890); + assert_eq!(*bn, 12345); + assert_eq!(*th, tx_hash); + } + _ => panic!("Expected Included event"), + } + } + + #[test] + fn test_update_userop_history_transform_handles_all_event_types() { + let userop_history = UserOpHistory { history: vec![] }; + let user_op_hash = B256::from([1u8; 32]); + let sender = Address::from([2u8; 20]); + let entry_point = Address::from([3u8; 20]); + let nonce = U256::from(1); + + let userop_event = UserOpEvent::AddedToMempool { user_op_hash, sender, entry_point, nonce }; + let event = create_test_userop_event("key-1", 1234567890, userop_event); + let result = update_userop_history_transform(userop_history.clone(), &event); + assert!(result.is_some()); + + let userop_event = UserOpEvent::Dropped { + user_op_hash, + reason: UserOpDropReason::Invalid("test error".to_string()), + }; + let event = create_test_userop_event("key-2", 1234567891, userop_event); + let result = update_userop_history_transform(userop_history.clone(), &event); + assert!(result.is_some()); + + let userop_event = UserOpEvent::Included { + user_op_hash, + block_number: 12345, + tx_hash: TxHash::from([4u8; 32]), + }; + let event = create_test_userop_event("key-3", 1234567892, userop_event); + let result = update_userop_history_transform(userop_history, &event); + assert!(result.is_some()); + } + + #[test] + fn test_userop_history_event_key_accessor() { + let sender = Address::from([2u8; 20]); + let entry_point = Address::from([3u8; 20]); + let nonce = U256::from(1); + + let event1 = UserOpHistoryEvent::AddedToMempool { + key: "key-1".to_string(), + timestamp: 1234567890, + sender, + entry_point, + nonce, + }; + assert_eq!(event1.key(), "key-1"); + + let event2 = UserOpHistoryEvent::Dropped { + key: "key-2".to_string(), + timestamp: 1234567890, + reason: UserOpDropReason::Expired, + }; + assert_eq!(event2.key(), "key-2"); + + let event3 = UserOpHistoryEvent::Included { + key: "key-3".to_string(), + timestamp: 1234567890, + block_number: 12345, + tx_hash: TxHash::from([4u8; 32]), + }; + assert_eq!(event3.key(), "key-3"); + } + + #[test] + fn test_userop_history_serialization() { + let sender = Address::from([2u8; 20]); + let entry_point = Address::from([3u8; 20]); + let nonce = U256::from(1); + + let history = UserOpHistory { + history: vec![UserOpHistoryEvent::AddedToMempool { + key: "test-key".to_string(), + timestamp: 1234567890, + sender, + entry_point, + nonce, + }], + }; + + let json = serde_json::to_string(&history).unwrap(); + let deserialized: UserOpHistory = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.history.len(), 1); + assert_eq!(deserialized.history[0].key(), "test-key"); + } +} diff --git a/crates/tips/audit/src/types.rs b/crates/tips/audit/src/types.rs new file mode 100644 index 0000000..3786c9f --- /dev/null +++ b/crates/tips/audit/src/types.rs @@ -0,0 +1,318 @@ +use alloy_consensus::transaction::{SignerRecoverable, Transaction as ConsensusTransaction}; +use alloy_primitives::{Address, B256, TxHash, U256}; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use tips_core::AcceptedBundle; +use uuid::Uuid; + +/// Unique identifier for a transaction. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct TransactionId { + /// The sender address. + pub sender: Address, + /// The transaction nonce. + pub nonce: U256, + /// The transaction hash. + pub hash: TxHash, +} + +/// Unique identifier for a bundle. +pub type BundleId = Uuid; + +/// Reason a bundle was dropped. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DropReason { + /// Bundle timed out. + TimedOut, + /// Bundle transaction reverted. + Reverted, +} + +/// A transaction with its data. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Transaction { + /// Transaction identifier. + pub id: TransactionId, + /// Raw transaction data. + pub data: Bytes, +} + +/// Hash of a user operation. +pub type UserOpHash = B256; + +/// Reason a user operation was dropped. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum UserOpDropReason { + /// User operation was invalid. + Invalid(String), + /// User operation expired. + Expired, + /// Replaced by a higher fee user operation. + ReplacedByHigherFee, +} + +/// Bundle lifecycle event. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "event", content = "data")] +pub enum BundleEvent { + /// Bundle was received. + Received { + /// Bundle identifier. + bundle_id: BundleId, + /// The accepted bundle. + bundle: Box, + }, + /// Bundle was cancelled. + Cancelled { + /// Bundle identifier. + bundle_id: BundleId, + }, + /// Bundle was included by a builder. + BuilderIncluded { + /// Bundle identifier. + bundle_id: BundleId, + /// Builder identifier. + builder: String, + /// Block number. + block_number: u64, + /// Flashblock index. + flashblock_index: u64, + }, + /// Bundle was included in a block. + BlockIncluded { + /// Bundle identifier. + bundle_id: BundleId, + /// Block number. + block_number: u64, + /// Block hash. + block_hash: TxHash, + }, + /// Bundle was dropped. + Dropped { + /// Bundle identifier. + bundle_id: BundleId, + /// Drop reason. + reason: DropReason, + }, +} + +impl BundleEvent { + /// Returns the bundle ID for this event. + pub const fn bundle_id(&self) -> BundleId { + match self { + Self::Received { bundle_id, .. } + | Self::Cancelled { bundle_id, .. } + | Self::BuilderIncluded { bundle_id, .. } + | Self::BlockIncluded { bundle_id, .. } + | Self::Dropped { bundle_id, .. } => *bundle_id, + } + } + + /// Returns transaction IDs from this event (only for Received events). + pub fn transaction_ids(&self) -> Vec { + match self { + Self::Received { bundle, .. } => bundle + .txs + .iter() + .filter_map(|envelope| { + envelope.recover_signer().ok().map(|sender| TransactionId { + sender, + nonce: U256::from(envelope.nonce()), + hash: *envelope.hash(), + }) + }) + .collect(), + Self::Cancelled { .. } + | Self::BuilderIncluded { .. } + | Self::BlockIncluded { .. } + | Self::Dropped { .. } => vec![], + } + } + + /// Generates a unique event key for this event. + pub fn generate_event_key(&self) -> String { + match self { + Self::BlockIncluded { bundle_id, block_hash, .. } => { + format!("{bundle_id}-{block_hash}") + } + _ => { + format!( + "{}-{}", + self.bundle_id(), + Uuid::new_v5(&Uuid::NAMESPACE_OID, self.bundle_id().as_bytes()) + ) + } + } + } +} + +/// User operation lifecycle event. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "event", content = "data")] +pub enum UserOpEvent { + /// User operation was added to the mempool. + AddedToMempool { + /// Hash of the user operation. + user_op_hash: UserOpHash, + /// Sender address. + sender: Address, + /// Entry point address. + entry_point: Address, + /// Nonce. + nonce: U256, + }, + /// User operation was dropped. + Dropped { + /// Hash of the user operation. + user_op_hash: UserOpHash, + /// Reason for dropping. + reason: UserOpDropReason, + }, + /// User operation was included in a block. + Included { + /// Hash of the user operation. + user_op_hash: UserOpHash, + /// Block number. + block_number: u64, + /// Transaction hash. + tx_hash: TxHash, + }, +} + +impl UserOpEvent { + /// Returns the user operation hash for this event. + pub const fn user_op_hash(&self) -> UserOpHash { + match self { + Self::AddedToMempool { user_op_hash, .. } + | Self::Dropped { user_op_hash, .. } + | Self::Included { user_op_hash, .. } => *user_op_hash, + } + } + + /// Generates a unique event key for this event. + pub fn generate_event_key(&self) -> String { + match self { + Self::Included { user_op_hash, tx_hash, .. } => { + format!("{user_op_hash}-{tx_hash}") + } + _ => { + format!( + "{}-{}", + self.user_op_hash(), + Uuid::new_v5(&Uuid::NAMESPACE_OID, self.user_op_hash().as_slice()) + ) + } + } + } +} + +#[cfg(test)] +mod user_op_event_tests { + use alloy_primitives::{address, b256}; + + use super::*; + + fn create_test_user_op_hash() -> UserOpHash { + b256!("1111111111111111111111111111111111111111111111111111111111111111") + } + + #[test] + fn test_user_op_event_added_to_mempool_serialization() { + let event = UserOpEvent::AddedToMempool { + user_op_hash: create_test_user_op_hash(), + sender: address!("2222222222222222222222222222222222222222"), + entry_point: address!("0000000071727De22E5E9d8BAf0edAc6f37da032"), + nonce: U256::from(1), + }; + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("\"event\":\"AddedToMempool\"")); + + let deserialized: UserOpEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(event.user_op_hash(), deserialized.user_op_hash()); + } + + #[test] + fn test_user_op_event_dropped_serialization() { + let event = UserOpEvent::Dropped { + user_op_hash: create_test_user_op_hash(), + reason: UserOpDropReason::Invalid("gas too low".to_string()), + }; + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("\"event\":\"Dropped\"")); + assert!(json.contains("gas too low")); + + let deserialized: UserOpEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(event.user_op_hash(), deserialized.user_op_hash()); + } + + #[test] + fn test_user_op_event_included_serialization() { + let event = UserOpEvent::Included { + user_op_hash: create_test_user_op_hash(), + block_number: 12345, + tx_hash: b256!("3333333333333333333333333333333333333333333333333333333333333333"), + }; + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("\"event\":\"Included\"")); + assert!(json.contains("\"block_number\":12345")); + + let deserialized: UserOpEvent = serde_json::from_str(&json).unwrap(); + assert_eq!(event.user_op_hash(), deserialized.user_op_hash()); + } + + #[test] + fn test_user_op_hash_accessor() { + let hash = create_test_user_op_hash(); + + let added = UserOpEvent::AddedToMempool { + user_op_hash: hash, + sender: address!("2222222222222222222222222222222222222222"), + entry_point: address!("0000000071727De22E5E9d8BAf0edAc6f37da032"), + nonce: U256::from(1), + }; + assert_eq!(added.user_op_hash(), hash); + + let dropped = + UserOpEvent::Dropped { user_op_hash: hash, reason: UserOpDropReason::Expired }; + assert_eq!(dropped.user_op_hash(), hash); + + let included = UserOpEvent::Included { + user_op_hash: hash, + block_number: 100, + tx_hash: b256!("4444444444444444444444444444444444444444444444444444444444444444"), + }; + assert_eq!(included.user_op_hash(), hash); + } + + #[test] + fn test_generate_event_key_included() { + let user_op_hash = + b256!("1111111111111111111111111111111111111111111111111111111111111111"); + let tx_hash = b256!("2222222222222222222222222222222222222222222222222222222222222222"); + + let event = UserOpEvent::Included { user_op_hash, block_number: 100, tx_hash }; + + let key = event.generate_event_key(); + assert!(key.contains(&format!("{user_op_hash}"))); + assert!(key.contains(&format!("{tx_hash}"))); + } + + #[test] + fn test_user_op_drop_reason_variants() { + let invalid = UserOpDropReason::Invalid("test reason".to_string()); + let json = serde_json::to_string(&invalid).unwrap(); + assert!(json.contains("Invalid")); + assert!(json.contains("test reason")); + + let expired = UserOpDropReason::Expired; + let json = serde_json::to_string(&expired).unwrap(); + assert!(json.contains("Expired")); + + let replaced = UserOpDropReason::ReplacedByHigherFee; + let json = serde_json::to_string(&replaced).unwrap(); + assert!(json.contains("ReplacedByHigherFee")); + } +} diff --git a/crates/tips/audit/tests/common/mod.rs b/crates/tips/audit/tests/common/mod.rs new file mode 100644 index 0000000..36609ab --- /dev/null +++ b/crates/tips/audit/tests/common/mod.rs @@ -0,0 +1,74 @@ +use rdkafka::{ClientConfig, consumer::StreamConsumer, producer::FutureProducer}; +use testcontainers::runners::AsyncRunner; +use testcontainers_modules::{kafka, kafka::Kafka, minio::MinIO}; +use tips_core::{BundleExtensions, test_utils::create_bundle_from_txn_data}; +use uuid::Uuid; + +pub(crate) struct TestHarness { + pub s3_client: aws_sdk_s3::Client, + pub bucket_name: String, + #[allow(dead_code)] // TODO is read + pub kafka_producer: FutureProducer, + #[allow(dead_code)] // TODO is read + pub kafka_consumer: StreamConsumer, + _minio_container: testcontainers::ContainerAsync, + _kafka_container: testcontainers::ContainerAsync, +} + +impl TestHarness { + pub(crate) async fn new() -> Result> { + let minio_container = MinIO::default().start().await?; + let s3_port = minio_container.get_host_port_ipv4(9000).await?; + let s3_endpoint = format!("http://127.0.0.1:{s3_port}"); + + let config = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region("us-east-1") + .endpoint_url(&s3_endpoint) + .credentials_provider(aws_sdk_s3::config::Credentials::new( + "minioadmin", + "minioadmin", + None, + None, + "test", + )) + .load() + .await; + + let s3_client = aws_sdk_s3::Client::new(&config); + let bundle = create_bundle_from_txn_data(); + let bucket_name = format!( + "test-bucket-{}", + Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()) + ); + + s3_client.create_bucket().bucket(&bucket_name).send().await?; + + let kafka_container = Kafka::default().start().await?; + let bootstrap_servers = + format!("127.0.0.1:{}", kafka_container.get_host_port_ipv4(kafka::KAFKA_PORT).await?); + + let kafka_producer = ClientConfig::new() + .set("bootstrap.servers", &bootstrap_servers) + .set("message.timeout.ms", "5000") + .create::() + .expect("Failed to create Kafka FutureProducer"); + + let kafka_consumer = ClientConfig::new() + .set("group.id", "testcontainer-rs") + .set("bootstrap.servers", &bootstrap_servers) + .set("session.timeout.ms", "6000") + .set("enable.auto.commit", "false") + .set("auto.offset.reset", "earliest") + .create::() + .expect("Failed to create Kafka StreamConsumer"); + + Ok(Self { + s3_client, + bucket_name, + kafka_producer, + kafka_consumer, + _minio_container: minio_container, + _kafka_container: kafka_container, + }) + } +} diff --git a/crates/tips/audit/tests/integration_tests.rs b/crates/tips/audit/tests/integration_tests.rs new file mode 100644 index 0000000..ed8a636 --- /dev/null +++ b/crates/tips/audit/tests/integration_tests.rs @@ -0,0 +1,115 @@ +#![allow(missing_docs)] + +use std::time::Duration; + +use alloy_primitives::{Address, B256, U256}; +use tips_audit_lib::{ + BundleEvent, BundleEventPublisher, BundleEventS3Reader, DropReason, KafkaAuditArchiver, + KafkaAuditLogReader, KafkaBundleEventPublisher, KafkaUserOpAuditLogReader, + KafkaUserOpEventPublisher, S3EventReaderWriter, UserOpEvent, UserOpEventPublisher, + UserOpEventReader, +}; +use tips_core::{BundleExtensions, test_utils::create_bundle_from_txn_data}; +use uuid::Uuid; +mod common; +use common::TestHarness; + +#[tokio::test] +#[ignore = "TODO doesn't appear to work with minio, should test against a real S3 bucket"] +async fn test_kafka_publisher_s3_archiver_integration() +-> Result<(), Box> { + let harness = TestHarness::new().await?; + let topic = "test-mempool-events"; + + let s3_writer = + S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let bundle = create_bundle_from_txn_data(); + let test_bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + let test_events = [ + BundleEvent::Received { bundle_id: test_bundle_id, bundle: Box::new(bundle.clone()) }, + BundleEvent::Dropped { bundle_id: test_bundle_id, reason: DropReason::TimedOut }, + ]; + + let publisher = KafkaBundleEventPublisher::new(harness.kafka_producer, topic.to_string()); + + for event in &test_events { + publisher.publish(event.clone()).await?; + } + + let mut consumer = KafkaAuditArchiver::new( + KafkaAuditLogReader::new(harness.kafka_consumer, topic.to_string())?, + s3_writer.clone(), + 1, + 100, + false, + ); + + tokio::spawn(async move { + consumer.run().await.expect("error running consumer"); + }); + + // Wait for the messages to be received + let mut counter = 0; + loop { + counter += 1; + if counter > 10 { + panic!("unable to complete archiving within the deadline"); + } + + tokio::time::sleep(Duration::from_secs(1)).await; + let bundle_history = s3_writer.get_bundle_history(test_bundle_id).await?; + + if let Some(history) = bundle_history { + if history.history.len() != test_events.len() { + continue; + } + break; + } + continue; + } + + Ok(()) +} + +#[tokio::test] +async fn test_userop_kafka_publisher_reader_integration() +-> Result<(), Box> { + let harness = TestHarness::new().await?; + let topic = "test-userop-events"; + + let test_user_op_hash = B256::from_slice(&[1u8; 32]); + let test_sender = Address::from_slice(&[2u8; 20]); + let test_entry_point = Address::from_slice(&[3u8; 20]); + let test_nonce = U256::from(42); + + let test_event = UserOpEvent::AddedToMempool { + user_op_hash: test_user_op_hash, + sender: test_sender, + entry_point: test_entry_point, + nonce: test_nonce, + }; + + let publisher = KafkaUserOpEventPublisher::new(harness.kafka_producer, topic.to_string()); + publisher.publish(test_event.clone()).await?; + + let mut reader = KafkaUserOpAuditLogReader::new(harness.kafka_consumer, topic.to_string())?; + + let received = tokio::time::timeout(Duration::from_secs(10), reader.read_event()).await??; + + assert_eq!(received.event.user_op_hash(), test_user_op_hash); + + match received.event { + UserOpEvent::AddedToMempool { user_op_hash, sender, entry_point, nonce } => { + assert_eq!(user_op_hash, test_user_op_hash); + assert_eq!(sender, test_sender); + assert_eq!(entry_point, test_entry_point); + assert_eq!(nonce, test_nonce); + } + _ => panic!("Expected AddedToMempool event"), + } + + reader.commit().await?; + + Ok(()) +} diff --git a/crates/tips/audit/tests/s3_test.rs b/crates/tips/audit/tests/s3_test.rs new file mode 100644 index 0000000..0d253e7 --- /dev/null +++ b/crates/tips/audit/tests/s3_test.rs @@ -0,0 +1,397 @@ +#![allow(missing_docs)] + +use std::sync::Arc; + +use alloy_primitives::{Address, B256, TxHash, U256}; +use tips_audit_lib::{ + BundleEvent, BundleEventS3Reader, Event, EventWriter, S3EventReaderWriter, UserOpDropReason, + UserOpEvent, UserOpEventS3Reader, UserOpEventWrapper, UserOpEventWriter, +}; +use tokio::task::JoinSet; +use uuid::Uuid; + +mod common; +use common::TestHarness; +use tips_core::{ + BundleExtensions, + test_utils::{TXN_HASH, create_bundle_from_txn_data}, +}; + +fn create_test_event(key: &str, timestamp: i64, bundle_event: BundleEvent) -> Event { + Event { key: key.to_string(), timestamp, event: bundle_event } +} + +#[tokio::test] +async fn test_event_write_and_read() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let bundle = create_bundle_from_txn_data(); + let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + let event = create_test_event( + "test-key-1", + 1234567890, + BundleEvent::Received { bundle_id, bundle: Box::new(bundle.clone()) }, + ); + + writer.archive_event(event).await?; + + let bundle_history = writer.get_bundle_history(bundle_id).await?; + assert!(bundle_history.is_some()); + + let history = bundle_history.unwrap(); + assert_eq!(history.history.len(), 1); + assert_eq!(history.history[0].key(), "test-key-1"); + + let metadata = writer.get_transaction_metadata(TXN_HASH).await?; + assert!(metadata.is_some()); + + if let Some(metadata) = metadata { + assert!(metadata.bundle_ids.contains(&bundle_id)); + } + + let bundle_id_two = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + let bundle = create_bundle_from_txn_data(); + let event = create_test_event( + "test-key-2", + 1234567890, + BundleEvent::Received { bundle_id: bundle_id_two, bundle: Box::new(bundle.clone()) }, + ); + + writer.archive_event(event).await?; + + let metadata = writer.get_transaction_metadata(TXN_HASH).await?; + assert!(metadata.is_some()); + + if let Some(metadata) = metadata { + assert!(metadata.bundle_ids.contains(&bundle_id)); + assert!(metadata.bundle_ids.contains(&bundle_id_two)); + } + + Ok(()) +} + +#[tokio::test] +async fn test_events_appended() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let bundle = create_bundle_from_txn_data(); + let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + + let events = [ + create_test_event( + "test-key-1", + 1234567890, + BundleEvent::Received { bundle_id, bundle: Box::new(bundle.clone()) }, + ), + create_test_event("test-key-2", 1234567891, BundleEvent::Cancelled { bundle_id }), + ]; + + for (idx, event) in events.iter().enumerate() { + writer.archive_event(event.clone()).await?; + + let bundle_history = writer.get_bundle_history(bundle_id).await?; + assert!(bundle_history.is_some()); + + let history = bundle_history.unwrap(); + assert_eq!(history.history.len(), idx + 1); + + let keys: Vec = history.history.iter().map(|e| e.key().to_string()).collect(); + assert_eq!( + keys, + events.iter().map(|e| e.key.clone()).take(idx + 1).collect::>() + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_event_deduplication() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let bundle = create_bundle_from_txn_data(); + let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + let bundle = create_bundle_from_txn_data(); + let event = create_test_event( + "duplicate-key", + 1234567890, + BundleEvent::Received { bundle_id, bundle: Box::new(bundle.clone()) }, + ); + + writer.archive_event(event.clone()).await?; + writer.archive_event(event).await?; + + let bundle_history = writer.get_bundle_history(bundle_id).await?; + assert!(bundle_history.is_some()); + + let history = bundle_history.unwrap(); + assert_eq!(history.history.len(), 1); + assert_eq!(history.history[0].key(), "duplicate-key"); + + Ok(()) +} + +#[tokio::test] +async fn test_nonexistent_data() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let bundle = create_bundle_from_txn_data(); + let nonexistent_bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + let bundle_history = writer.get_bundle_history(nonexistent_bundle_id).await?; + assert!(bundle_history.is_none()); + + let nonexistent_tx_hash = TxHash::from([255u8; 32]); + let metadata = writer.get_transaction_metadata(nonexistent_tx_hash).await?; + assert!(metadata.is_none()); + + Ok(()) +} + +#[tokio::test] +#[ignore = "TODO doesn't appear to work with minio, should test against a real S3 bucket"] +async fn test_concurrent_writes_for_bundle() -> Result<(), Box> +{ + let harness = TestHarness::new().await?; + let writer = + Arc::new(S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone())); + + let bundle = create_bundle_from_txn_data(); + let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); + + let event = create_test_event( + "hello-dan", + 1234567889i64, + BundleEvent::Received { bundle_id, bundle: Box::new(bundle.clone()) }, + ); + + writer.archive_event(event.clone()).await?; + + let mut join_set: JoinSet>> = + JoinSet::new(); + + for i in 0..4 { + let writer_clone = Arc::clone(&writer); + let key = if i % 4 == 0 { "shared-key".to_string() } else { format!("unique-key-{i}") }; + + let event = create_test_event( + &key, + 1234567890 + i as i64, + BundleEvent::Received { bundle_id, bundle: Box::new(bundle.clone()) }, + ); + + join_set.spawn(async move { + writer_clone.archive_event(event.clone()).await.map_err(|e| e.into()) + }); + } + + let tasks = join_set.join_all().await; + assert_eq!(tasks.len(), 4); + for t in &tasks { + assert!(t.is_ok()); + } + + let bundle_history = writer.get_bundle_history(bundle_id).await?; + assert!(bundle_history.is_some()); + + let history = bundle_history.unwrap(); + + let shared_count = history.history.iter().filter(|e| e.key() == "shared-key").count(); + assert_eq!(shared_count, 1); + + let unique_count = + history.history.iter().filter(|e| e.key().starts_with("unique-key-")).count(); + assert_eq!(unique_count, 3); + + assert_eq!(history.history.len(), 4); + + Ok(()) +} + +fn create_test_userop_event( + key: &str, + timestamp: i64, + userop_event: UserOpEvent, +) -> UserOpEventWrapper { + UserOpEventWrapper { key: key.to_string(), timestamp, event: userop_event } +} + +#[tokio::test] +async fn test_userop_event_write_and_read() -> Result<(), Box> +{ + let harness = TestHarness::new().await?; + let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let user_op_hash = B256::from([1u8; 32]); + let sender = Address::from([2u8; 20]); + let entry_point = Address::from([3u8; 20]); + let nonce = U256::from(1); + + let event = create_test_userop_event( + "test-userop-key-1", + 1234567890, + UserOpEvent::AddedToMempool { user_op_hash, sender, entry_point, nonce }, + ); + + writer.archive_userop_event(event).await?; + + let userop_history = writer.get_userop_history(user_op_hash).await?; + assert!(userop_history.is_some()); + + let history = userop_history.unwrap(); + assert_eq!(history.history.len(), 1); + assert_eq!(history.history[0].key(), "test-userop-key-1"); + + Ok(()) +} + +#[tokio::test] +async fn test_userop_events_appended() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let user_op_hash = B256::from([10u8; 32]); + let sender = Address::from([11u8; 20]); + let entry_point = Address::from([12u8; 20]); + let nonce = U256::from(1); + let tx_hash = TxHash::from([13u8; 32]); + + let events = [ + create_test_userop_event( + "userop-key-1", + 1234567890, + UserOpEvent::AddedToMempool { user_op_hash, sender, entry_point, nonce }, + ), + create_test_userop_event( + "userop-key-2", + 1234567891, + UserOpEvent::Included { user_op_hash, block_number: 12345, tx_hash }, + ), + ]; + + for (idx, event) in events.iter().enumerate() { + writer.archive_userop_event(event.clone()).await?; + + let userop_history = writer.get_userop_history(user_op_hash).await?; + assert!(userop_history.is_some()); + + let history = userop_history.unwrap(); + assert_eq!(history.history.len(), idx + 1); + + let keys: Vec = history.history.iter().map(|e| e.key().to_string()).collect(); + assert_eq!( + keys, + events.iter().map(|e| e.key.clone()).take(idx + 1).collect::>() + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_userop_event_deduplication() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let user_op_hash = B256::from([20u8; 32]); + let sender = Address::from([21u8; 20]); + let entry_point = Address::from([22u8; 20]); + let nonce = U256::from(1); + + let event = create_test_userop_event( + "duplicate-userop-key", + 1234567890, + UserOpEvent::AddedToMempool { user_op_hash, sender, entry_point, nonce }, + ); + + writer.archive_userop_event(event.clone()).await?; + writer.archive_userop_event(event).await?; + + let userop_history = writer.get_userop_history(user_op_hash).await?; + assert!(userop_history.is_some()); + + let history = userop_history.unwrap(); + assert_eq!(history.history.len(), 1); + assert_eq!(history.history[0].key(), "duplicate-userop-key"); + + Ok(()) +} + +#[tokio::test] +async fn test_userop_nonexistent_returns_none() +-> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let nonexistent_hash = B256::from([255u8; 32]); + let userop_history = writer.get_userop_history(nonexistent_hash).await?; + assert!(userop_history.is_none()); + + Ok(()) +} + +#[tokio::test] +async fn test_userop_all_event_types() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let user_op_hash = B256::from([30u8; 32]); + let sender = Address::from([31u8; 20]); + let entry_point = Address::from([32u8; 20]); + let nonce = U256::from(1); + let tx_hash = TxHash::from([33u8; 32]); + + let event1 = create_test_userop_event( + "event-added", + 1234567890, + UserOpEvent::AddedToMempool { user_op_hash, sender, entry_point, nonce }, + ); + writer.archive_userop_event(event1).await?; + + let event2 = create_test_userop_event( + "event-included", + 1234567891, + UserOpEvent::Included { user_op_hash, block_number: 12345, tx_hash }, + ); + writer.archive_userop_event(event2).await?; + + let userop_history = writer.get_userop_history(user_op_hash).await?; + assert!(userop_history.is_some()); + + let history = userop_history.unwrap(); + assert_eq!(history.history.len(), 2); + assert_eq!(history.history[0].key(), "event-added"); + assert_eq!(history.history[1].key(), "event-included"); + + Ok(()) +} + +#[tokio::test] +async fn test_userop_dropped_event() -> Result<(), Box> { + let harness = TestHarness::new().await?; + let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); + + let user_op_hash = B256::from([40u8; 32]); + + let event = create_test_userop_event( + "event-dropped", + 1234567890, + UserOpEvent::Dropped { + user_op_hash, + reason: UserOpDropReason::Invalid("AA21 didn't pay prefund".to_string()), + }, + ); + writer.archive_userop_event(event).await?; + + let userop_history = writer.get_userop_history(user_op_hash).await?; + assert!(userop_history.is_some()); + + let history = userop_history.unwrap(); + assert_eq!(history.history.len(), 1); + assert_eq!(history.history[0].key(), "event-dropped"); + + Ok(()) +} diff --git a/crates/tips/core/Cargo.toml b/crates/tips/core/Cargo.toml new file mode 100644 index 0000000..5ab4237 --- /dev/null +++ b/crates/tips/core/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "tips-core" +description = "Core primitives and utilities for TIPS" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[features] +test-utils = [ "dep:alloy-signer-local", "dep:op-alloy-rpc-types" ] + +[dependencies] +tracing = { workspace = true, features = ["std"] } +alloy-consensus = { workspace = true, features = ["std"] } +alloy-rpc-types = { workspace = true, features = ["eth"] } +alloy-provider = { workspace = true, features = ["reqwest"] } +alloy-signer-local = { workspace = true, optional = true } +op-alloy-consensus = { workspace = true, features = ["std", "k256", "serde"] } +alloy-primitives = { workspace = true, features = ["map-foldhash", "serde"] } +op-alloy-rpc-types = { workspace = true, features = ["std"], optional = true } +metrics-exporter-prometheus = { workspace = true, features = ["http-listener"] } +tracing-subscriber = { workspace = true, features = ["std", "fmt", "ansi", "env-filter", "json"] } + +base-bundles = { workspace = true } + +[dev-dependencies] +alloy-signer-local.workspace = true +serde_json = { workspace = true, features = ["std"] } +op-alloy-rpc-types = { workspace = true, features = ["std"] } diff --git a/crates/tips/core/README.md b/crates/tips/core/README.md new file mode 100644 index 0000000..6dd1d3d --- /dev/null +++ b/crates/tips/core/README.md @@ -0,0 +1,3 @@ +# `tips-core` + +Core primitives and utilities for TIPS. diff --git a/crates/tips/core/src/kafka.rs b/crates/tips/core/src/kafka.rs new file mode 100644 index 0000000..ac07051 --- /dev/null +++ b/crates/tips/core/src/kafka.rs @@ -0,0 +1,21 @@ +use std::{collections::HashMap, fs}; + +pub fn load_kafka_config_from_file( + properties_file_path: &str, +) -> Result, std::io::Error> { + let kafka_properties = fs::read_to_string(properties_file_path)?; + + let mut config = HashMap::new(); + + for line in kafka_properties.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some((key, value)) = line.split_once('=') { + config.insert(key.trim().to_string(), value.trim().to_string()); + } + } + + Ok(config) +} diff --git a/crates/tips/core/src/lib.rs b/crates/tips/core/src/lib.rs new file mode 100644 index 0000000..9baa41e --- /dev/null +++ b/crates/tips/core/src/lib.rs @@ -0,0 +1,18 @@ +#![doc = include_str!("../README.md")] +#![doc(issue_tracker_base_url = "https://github.com/base/tips/issues/")] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![allow(missing_docs)] + +use alloy_rpc_types as _; + +pub mod kafka; +pub mod logger; +pub mod metrics; +#[cfg(any(test, feature = "test-utils"))] +pub mod test_utils; + +pub use base_bundles::{ + AcceptedBundle, Bundle, BundleExtensions, BundleHash, BundleTxs, CancelBundle, + MeterBundleResponse, ParsedBundle, +}; diff --git a/crates/tips/core/src/logger.rs b/crates/tips/core/src/logger.rs new file mode 100644 index 0000000..ccdb8d2 --- /dev/null +++ b/crates/tips/core/src/logger.rs @@ -0,0 +1,63 @@ +use std::str::FromStr; + +use tracing::warn; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LogFormat { + Pretty, + Json, + Compact, +} + +impl FromStr for LogFormat { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "json" => Ok(Self::Json), + "compact" => Ok(Self::Compact), + "pretty" => Ok(Self::Pretty), + _ => { + warn!("Invalid log format '{}', defaulting to 'pretty'", s); + Ok(Self::Pretty) + } + } + } +} + +pub fn init_logger(log_level: &str) { + init_logger_with_format(log_level, LogFormat::Pretty); +} + +pub fn init_logger_with_format(log_level: &str, format: LogFormat) { + let level = match log_level.to_lowercase().as_str() { + "trace" => tracing::Level::TRACE, + "debug" => tracing::Level::DEBUG, + "info" => tracing::Level::INFO, + "warn" => tracing::Level::WARN, + "error" => tracing::Level::ERROR, + _ => { + warn!("Invalid log level '{}', defaulting to 'info'", log_level); + tracing::Level::INFO + } + }; + + let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(level.to_string())); + + match format { + LogFormat::Json => { + tracing_subscriber::registry() + .with(env_filter) + .with(fmt::layer().json().flatten_event(true).with_current_span(true)) + .init(); + } + LogFormat::Compact => { + tracing_subscriber::registry().with(env_filter).with(fmt::layer().compact()).init(); + } + LogFormat::Pretty => { + tracing_subscriber::registry().with(env_filter).with(fmt::layer().pretty()).init(); + } + } +} diff --git a/crates/tips/core/src/metrics.rs b/crates/tips/core/src/metrics.rs new file mode 100644 index 0000000..bb5f67f --- /dev/null +++ b/crates/tips/core/src/metrics.rs @@ -0,0 +1,10 @@ +use std::net::SocketAddr; + +use metrics_exporter_prometheus::PrometheusBuilder; + +pub fn init_prometheus_exporter(addr: SocketAddr) -> Result<(), Box> { + PrometheusBuilder::new() + .with_http_listener(addr) + .install() + .map_err(|e| Box::new(e) as Box) +} diff --git a/crates/tips/core/src/test_utils.rs b/crates/tips/core/src/test_utils.rs new file mode 100644 index 0000000..6285ce4 --- /dev/null +++ b/crates/tips/core/src/test_utils.rs @@ -0,0 +1,75 @@ +use alloy_consensus::SignableTransaction; +use alloy_primitives::{Address, B256, Bytes, TxHash, U256, b256, bytes}; +use alloy_provider::network::{TxSignerSync, eip2718::Encodable2718}; +use alloy_signer_local::PrivateKeySigner; +use op_alloy_consensus::OpTxEnvelope; +use op_alloy_rpc_types::OpTransactionRequest; + +use crate::{AcceptedBundle, Bundle, MeterBundleResponse}; + +// https://basescan.org/tx/0x4f7ddfc911f5cf85dd15a413f4cbb2a0abe4f1ff275ed13581958c0bcf043c5e +pub const TXN_DATA: Bytes = bytes!( + "0x02f88f8221058304b6b3018315fb3883124f80948ff2f0a8d017c79454aa28509a19ab9753c2dd1480a476d58e1a0182426068c9ea5b00000000000000000002f84f00000000083e4fda54950000c080a086fbc7bbee41f441fb0f32f7aa274d2188c460fe6ac95095fa6331fa08ec4ce7a01aee3bcc3c28f7ba4e0c24da9ae85e9e0166c73cabb42c25ff7b5ecd424f3105" +); + +pub const TXN_HASH: TxHash = + b256!("0x4f7ddfc911f5cf85dd15a413f4cbb2a0abe4f1ff275ed13581958c0bcf043c5e"); + +pub fn create_bundle_from_txn_data() -> AcceptedBundle { + AcceptedBundle::new( + Bundle { txs: vec![TXN_DATA], ..Default::default() }.try_into().unwrap(), + create_test_meter_bundle_response(), + ) +} + +pub fn create_transaction(from: PrivateKeySigner, nonce: u64, to: Address) -> OpTxEnvelope { + let mut txn = OpTransactionRequest::default() + .value(U256::from(10_000)) + .gas_limit(21_000) + .max_fee_per_gas(200) + .max_priority_fee_per_gas(100) + .from(from.address()) + .to(to) + .nonce(nonce) + .build_typed_tx() + .unwrap(); + + let sig = from.sign_transaction_sync(&mut txn).unwrap(); + OpTxEnvelope::Eip1559(txn.eip1559().cloned().unwrap().into_signed(sig)) +} + +pub fn create_test_bundle( + txns: Vec, + block_number: Option, + min_timestamp: Option, + max_timestamp: Option, +) -> AcceptedBundle { + let txs = txns.iter().map(|t| t.encoded_2718().into()).collect(); + + let bundle = Bundle { + txs, + block_number: block_number.unwrap_or(0), + min_timestamp, + max_timestamp, + ..Default::default() + }; + let meter_bundle_response = create_test_meter_bundle_response(); + + AcceptedBundle::new(bundle.try_into().unwrap(), meter_bundle_response) +} + +pub fn create_test_meter_bundle_response() -> MeterBundleResponse { + MeterBundleResponse { + bundle_gas_price: U256::from(0), + bundle_hash: B256::default(), + coinbase_diff: U256::from(0), + eth_sent_to_coinbase: U256::from(0), + gas_fees: U256::from(0), + results: vec![], + state_block_number: 0, + state_flashblock_index: None, + total_gas_used: 0, + total_execution_time_us: 0, + state_root_time_us: 0, + } +} diff --git a/deny.toml b/deny.toml index 62b04cd..2043fdf 100644 --- a/deny.toml +++ b/deny.toml @@ -44,9 +44,53 @@ skip = [ # Common ecosystem duplicates "getrandom", "hashbrown", + "itertools", + "foldhash", + "lru", + + # AWS SDK transitive dependencies + "aws-smithy-http", + "aws-smithy-json", + + # HTTP ecosystem (hyper 0.14 vs 1.x transition) + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + + # TLS/crypto ecosystem + "rustls", + "rustls-webpki", + "tokio-rustls", + + # macOS security framework + "core-foundation", + "security-framework", + "openssl-probe", + + # Random number generation + "rand", + "rand_chacha", + "rand_core", + + # Networking + "socket2", + + # OP Stack crates (reth uses older versions) + "op-alloy-consensus", # Windows platform crates "windows-sys", + "windows-targets", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [sources] diff --git a/justfile b/justfile index 5d528eb..2041465 100644 --- a/justfile +++ b/justfile @@ -101,3 +101,11 @@ docker-image: # Run mempool rebroadcaster service run-mempool-rebroadcaster: cargo run -p mempool-rebroadcaster -- --geth-mempool-endpoint {{env_var("GETH_MEMPOOL_ENDPOINT")}} --reth-mempool-endpoint {{env_var("RETH_MEMPOOL_ENDPOINT")}} + +# Run tips audit service +run-tips-audit: + cargo run -p tips-audit + +# Run tips ingress RPC service +run-tips-ingress-rpc: + cargo run -p tips-ingress-rpc