diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000..7053c63 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,8 @@ +[test-groups] +# Limit concurrency for tests that spin up Docker containers (Kafka + MinIO) +# to avoid resource contention causing container startup failures. +container-tests = { max-threads = 4 } + +[[profile.default.overrides]] +filter = 'binary_id(audit::s3_test) | binary_id(audit::integration_tests)' +test-group = 'container-tests' 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 ccf4f1d..8cc4f56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[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" @@ -51,9 +63,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86debde32d8dbb0ab29e7cc75ae1a98688ac7a4c9da54b3a9b14593b9b3c46d3" +checksum = "4e4ff99651d46cef43767b5e8262ea228cd05287409ccb0c947cc25e70a952f9" dependencies = [ "alloy-eips", "alloy-primitives", @@ -78,9 +90,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6cb2e7efd385b333f5a77b71baaa2605f7e22f1d583f2879543b54cbce777c" +checksum = "1a0701b0eda8051a2398591113e7862f807ccdd3315d0b441f06c2a0865a379b" dependencies = [ "alloy-consensus", "alloy-eips", @@ -92,9 +104,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668859fcdb42eee289de22a9d01758c910955bb6ecda675b97276f99ce2e16b0" +checksum = "f3c83c7a3c4e1151e8cac383d0a67ddf358f37e5ea51c95a1283d897c9de0a5a" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -114,9 +126,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14ff5ee5f27aa305bda825c735f686ad71bb65508158f059f513895abe69b8c3" +checksum = "e6ab1b2f1b48a7e6b3597cb2afae04f93879fb69d71e39736b5663d7366b23f2" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -162,6 +174,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "borsh", + "k256", "serde", "thiserror", ] @@ -180,9 +193,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be47bf1b91674a5f394b9ed3c691d764fb58ba43937f1371550ff4bc8e59c295" +checksum = "def1626eea28d48c6cc0a6f16f34d4af0001906e4f889df6c660b39c86fd044d" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -204,9 +217,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8708475665cc00e081c085886e68eada2f64cfa08fc668213a9231655093d4de" +checksum = "1e414aa37b335ad2acb78a95814c59d137d53139b412f87aed1e10e2d862cd49" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -216,13 +229,13 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a24c81a56d684f525cd1c012619815ad3a1dd13b0238f069356795d84647d3c" +checksum = "e57586581f2008933241d16c3e3f633168b3a5d2738c5c42ea5246ec5e0ef17a" dependencies = [ "alloy-primitives", "alloy-sol-types", - "http", + "http 1.4.0", "serde", "serde_json", "thiserror", @@ -231,9 +244,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786c5b3ad530eaf43cda450f973fe7fb1c127b4c8990adf66709dafca25e3f6f" +checksum = "3b36c2a0ed74e48851f78415ca5b465211bd678891ba11e88fee09eac534bab1" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -257,9 +270,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ed40adf21ae4be786ef5eb62db9c692f6a30f86d34452ca3f849d6390ce319" +checksum = "636c8051da58802e757b76c3b65af610b95799f72423dc955737dec73de234fd" dependencies = [ "alloy-consensus", "alloy-eips", @@ -270,9 +283,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b88cf92ed20685979ed1d8472422f0c6c2d010cec77caf63aaa7669cc1a7bc2" +checksum = "66b1483f8c2562bf35f0270b697d5b5fe8170464e935bd855a4c5eaf6f89b354" dependencies = [ "alloy-rlp", "bytes", @@ -297,9 +310,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ca4c15818be7ac86208aff3a91b951d14c24e1426e66624e75f2215ba5e2cc" +checksum = "b3dd56e2eafe8b1803e325867ac2c8a4c73c9fb5f341ffd8347f9344458c5922" dependencies = [ "alloy-chains", "alloy-consensus", @@ -339,9 +352,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9eb9c9371738ac47f589e40aae6e418cb5f7436ad25b87575a7f94a60ccf43b" +checksum = "6eebf54983d4fccea08053c218ee5c288adf2e660095a243d0532a8070b43955" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -383,9 +396,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe0addad5b8197e851062b49dc47157444bced173b601d91e3f9b561a060a50" +checksum = "91577235d341a1bdbee30a463655d08504408a4d51e9f72edbfc5a622829f402" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -408,11 +421,12 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d17d4645a717f0527e491f44f6f7a75c221b9c00ccf79ddba2d26c8e0df4c3" +checksum = "79cff039bf01a17d76c0aace3a3a773d5f895eb4c68baaae729ec9da9e86c99c" dependencies = [ "alloy-primitives", + "alloy-rpc-types-eth", "alloy-rpc-types-txpool", "alloy-serde", "serde", @@ -420,9 +434,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e98aabb013a71a4b67b52825f7b503e5bb6057fb3b7b2290d514b0b0574b57" +checksum = "73234a141ecce14e2989748c04fcac23deee67a445e2c4c167cfb42d4dacd1b6" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -431,9 +445,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc871ae69688e358cf242a6a7ee6b6e0476a03fd0256434c68daedaec086ec4" +checksum = "10620d600cc46538f613c561ac9a923843c6c74c61f054828dcdb8dd18c72ec4" dependencies = [ "alloy-consensus", "alloy-eips", @@ -449,9 +463,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5899af8417dcf89f40f88fa3bdb2f3f172605d8e167234311ee34811bbfdb0bf" +checksum = "010e101dbebe0c678248907a2545b574a87d078d82c2f6f5d0e8e7c9a6149a10" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -470,9 +484,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8074654c0292783d504bfa1f2691a69f420154ee9a7883f9212eaf611e60cd" +checksum = "14ab75189fbc29c5dd6f0bc1529bccef7b00773b458763f4d9d81a77ae4a1a2d" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -482,9 +496,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb73325ee881e42972a5a7bc85250f6af89f92c6ad1222285f74384a203abeb" +checksum = "9e6d631f8b975229361d8af7b2c749af31c73b3cf1352f90e144ddb06227105e" dependencies = [ "alloy-primitives", "serde", @@ -493,9 +507,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bea4c8f30eddb11d7ab56e83e49c814655daa78ca708df26c300c10d0189cbc" +checksum = "97f40010b5e8f79b70bf163b38cd15f529b18ca88c4427c0e43441ee54e4ed82" dependencies = [ "alloy-primitives", "async-trait", @@ -506,11 +520,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "alloy-signer-local" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c4ec1cc27473819399a3f0da83bc1cef0ceaac8c1c93997696e46dc74377a58" +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" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fa1ca7e617c634d2bd9fa71f9ec8e47c07106e248b9fcbd3eaddc13cabd625" +checksum = "2c4b64c8146291f750c3f391dff2dd40cf896f7e2b253417a31e342aa7265baa" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -522,9 +552,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c00c0c3a75150a9dc7c8c679ca21853a137888b4e1c5569f92d7e2b15b5102" +checksum = "d9df903674682f9bae8d43fdea535ab48df2d6a8cb5104ca29c58ada22ef67b3" dependencies = [ "alloy-sol-macro-input", "const-hex", @@ -540,9 +570,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-input" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297db260eb4d67c105f68d6ba11b8874eec681caec5505eab8fbebee97f790bc" +checksum = "737b8a959f527a86e07c44656db237024a32ae9b97d449f788262a547e8aa136" dependencies = [ "const-hex", "dunce", @@ -556,9 +586,9 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b91b13181d3bcd23680fd29d7bc861d1f33fbe90fdd0af67162434aeba902d" +checksum = "b28e6e86c6d2db52654b65a5a76b4f57eae5a32a7f0aa2222d1dbdb74e2cb8e0" dependencies = [ "serde", "winnow", @@ -566,9 +596,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc442cc2a75207b708d481314098a0f8b6f7b58e3148dd8d8cc7407b0d6f9385" +checksum = "fdf7effe4ab0a4f52c865959f790036e61a7983f68b13b75d7fbcedf20b753ce" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -578,13 +608,13 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b321f506bd67a434aae8e8a7dfe5373bf66137c149a5f09c9e7dfb0ca43d7c91" +checksum = "a03bb3f02b9a7ab23dacd1822fa7f69aa5c8eefcdcf57fad085e0b8d76fb4334" dependencies = [ "alloy-json-rpc", "auto_impl", - "base64", + "base64 0.22.1", "derive_more", "futures", "futures-utils-wasm", @@ -601,12 +631,13 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bf12879a20e1261cd39c3b101856f52d18886907a826e102538897f0d2b66e" +checksum = "5ce599598ef8ebe067f3627509358d9faaa1ef94f77f834a7783cd44209ef55c" dependencies = [ "alloy-json-rpc", "alloy-transport", + "itertools 0.14.0", "opentelemetry", "opentelemetry-http", "reqwest", @@ -619,14 +650,14 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527a0d9c8bbc5c3215b03ad465d4ae8775384ff5faec7c41f033f087c851a9f9" +checksum = "5ed38ea573c6658e0c2745af9d1f1773b1ed83aa59fbd9c286358ad469c3233a" dependencies = [ "alloy-pubsub", "alloy-transport", "futures", - "http", + "http 1.4.0", "serde_json", "tokio", "tokio-tungstenite", @@ -653,9 +684,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.6.1" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a91d6b4c2f6574fdbcb1611e460455c326667cf5b805c6bd1640dad8e8ee4d2" +checksum = "397406cf04b11ca2a48e6f81804c70af3f40a36abf648e11dc7416043eb0834d" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -672,56 +703,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.101" @@ -996,6 +983,54 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "audit" +version = "0.0.0" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-provider", + "alloy-signer-local", + "anyhow", + "async-trait", + "audit", + "aws-config", + "aws-sdk-s3", + "base-bundles", + "bytes", + "futures", + "metrics", + "metrics-derive", + "op-alloy-consensus 0.22.4", + "op-alloy-rpc-types", + "rdkafka", + "serde", + "serde_json", + "testcontainers", + "testcontainers-modules", + "tokio", + "tracing", + "utils", + "uuid", +] + +[[package]] +name = "audit-bin" +version = "0.0.0" +dependencies = [ + "anyhow", + "audit", + "aws-config", + "aws-credential-types", + "aws-sdk-s3", + "clap", + "dotenvy", + "rdkafka", + "tokio", + "tracing", + "utils", +] + [[package]] name = "auto_impl" version = "1.3.0" @@ -1014,9 +1049,436 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "base-flashtypes" +name = "aws-config" +version = "1.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http 0.63.4", + "aws-smithy-json 0.62.4", + "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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26bbf46abc608f2dc61fd6cb3b7b0665497cc259a21520151ed98f8b37d2c79" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f92058d22a46adf53ec57a6a96f34447daf02bff52e8fb956c66bcd5c6ac12" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http 0.63.4", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "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.98.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89c4f19655ab0856375e169865c91264de965bd74c407c7f1e403184b1049409" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http 0.63.4", + "aws-smithy-json 0.62.4", + "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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f6ae9b71597dc5fd115d52849d7a5556ad9265885ad3492ea8d73b93bbc46e" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http 0.63.4", + "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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cba48474f1d6807384d06fec085b909f5807e16653c5af5c45dfe89539f0b70" +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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c0b3e587fbaa5d7f7e870544508af8ce82ea47cd30376e69e1e37c4ac746f79" +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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4a8a5fe3e4ac7ee871237c340bbce13e982d37543b65700f4419e039f5d78e" +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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0709f0083aa19b704132684bc26d3c868e06bd428ccc4373b0b55c3e8748a58b" +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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd3dfc18c1ce097cf81fced7192731e63809829c6cbf933c1ec47452d08e1aa" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http 0.63.4", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c55e0837e9b8526f49e0b9bfa9ee18ddee70e853f5bc09c5d11ebceddcb0fec" +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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576b0d6991c9c32bc14fc340582ef148311f924d41815f641a308b5d11e8e7cd" +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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c50f3cdf47caa8d01f2be4a6663ea02418e892f9bbfd82c7b9a3a37eaccdd3a" +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#d8adce06b1b8d86f2ccdee5688004569f42789a2" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-provider", + "alloy-serde", + "op-alloy-consensus 0.23.1", + "op-alloy-flz", + "serde", + "uuid", +] + +[[package]] +name = "base-primitives" version = "0.0.0" -source = "git+https://github.com/base/base.git#720f6e1a73faabc938402b0cb617d3960db742d4" +source = "git+https://github.com/base/base.git#d8adce06b1b8d86f2ccdee5688004569f42789a2" dependencies = [ "alloy-primitives", "alloy-rpc-types-engine", @@ -1035,12 +1497,28 @@ 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" @@ -1069,12 +1547,12 @@ dependencies = [ "alloy-sol-types", "anyhow", "arboard", - "base-flashtypes", + "base-primitives", "chrono", "crossterm", "dirs", "futures-util", - "op-alloy-consensus", + "op-alloy-consensus 0.22.4", "op-alloy-network", "ratatui", "serde", @@ -1092,6 +1570,24 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" +[[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" @@ -1123,6 +1619,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" @@ -1147,19 +1649,69 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "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 = "blst" -version = "0.3.16" +name = "bollard-stubs" +version = "1.47.1-rc.27.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" dependencies = [ - "cc", - "glob", - "threadpool", - "zeroize", + "serde", + "serde_repr", + "serde_with", ] [[package]] @@ -1245,6 +1797,16 @@ 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" @@ -1262,9 +1824,9 @@ dependencies = [ [[package]] name = "cadence" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3075f133bee430b7644c54fb629b9b4420346ffa275a45c81a6babe8b09b4f51" +checksum = "d7aff0c323415907f37007d645d7499c378df47efb3e33ffc1f397fa4e549b2e" dependencies = [ "crossbeam-channel", ] @@ -1291,9 +1853,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" 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" @@ -1320,11 +1893,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.57" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" dependencies = [ "clap_builder", "clap_derive", @@ -1332,14 +1916,12 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" dependencies = [ - "anstream", "anstyle", "clap_lex", - "strsim", ] [[package]] @@ -1356,9 +1938,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clipboard-win" @@ -1370,10 +1952,13 @@ dependencies = [ ] [[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 = "compact_str" @@ -1446,6 +2031,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" @@ -1476,6 +2071,19 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +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" @@ -1494,6 +2102,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1506,7 +2123,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crossterm_winapi", "mio", "parking_lot", @@ -1655,9 +2272,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ "powerfmt", "serde_core", @@ -1745,7 +2362,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags", + "bitflags 2.10.0", "objc2", ] @@ -1760,6 +2377,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" @@ -1876,6 +2504,17 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +[[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" @@ -1943,6 +2582,17 @@ 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.9" @@ -2013,6 +2663,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" @@ -2160,6 +2816,19 @@ dependencies = [ "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "glob" version = "0.3.3" @@ -2177,6 +2846,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 = "half" version = "2.7.1" @@ -2260,6 +2967,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" @@ -2270,6 +2997,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" @@ -2277,7 +3015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.4.0", ] [[package]] @@ -2288,8 +3026,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -2299,6 +3037,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" @@ -2309,9 +3077,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", @@ -2320,6 +3090,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" @@ -2328,7 +3145,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-util", "native-tls", "tokio", @@ -2342,23 +3159,38 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "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" @@ -2464,6 +3296,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2586,12 +3424,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" @@ -2625,6 +3457,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" @@ -2641,7 +3483,7 @@ version = "9.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" dependencies = [ - "base64", + "base64 0.22.1", "js-sys", "pem", "ring", @@ -2687,13 +3529,29 @@ dependencies = [ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.181" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" + +[[package]] +name = "libloading" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] [[package]] name = "libm" @@ -2707,8 +3565,21 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags", + "bitflags 2.10.0", + "libc", + "redox_syscall 0.7.1", +] + +[[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]] @@ -2782,6 +3653,16 @@ dependencies = [ "regex-automata", ] +[[package]] +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.8.0" @@ -2803,6 +3684,7 @@ dependencies = [ "serde_json", "tokio", "tracing", + "tracing-subscriber", ] [[package]] @@ -2817,6 +3699,69 @@ dependencies = [ "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161ab904c2c62e7bda0f7562bf22f96440ca35ff79e66c800cbac298f2f4f5ec" +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 = "cdfb1365fea27e6dd9dc1dbc19f570198bc86914533ad639dae939635f096be4" +dependencies = [ + "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 = "miniz_oxide" version = "0.8.9" @@ -2858,14 +3803,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" @@ -2936,6 +3891,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", @@ -2970,7 +3926,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags", + "bitflags 2.10.0", "objc2", "objc2-core-graphics", "objc2-foundation", @@ -2982,7 +3938,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.10.0", "dispatch2", "objc2", ] @@ -2993,7 +3949,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags", + "bitflags 2.10.0", "dispatch2", "objc2", "objc2-core-foundation", @@ -3012,7 +3968,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", ] @@ -3023,7 +3979,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", ] @@ -3034,12 +3990,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - [[package]] name = "op-alloy-consensus" version = "0.22.4" @@ -3058,6 +4008,28 @@ dependencies = [ "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-network" version = "0.22.4" @@ -3070,7 +4042,7 @@ dependencies = [ "alloy-provider", "alloy-rpc-types-eth", "alloy-signer", - "op-alloy-consensus", + "op-alloy-consensus 0.22.4", "op-alloy-rpc-types", ] @@ -3087,7 +4059,7 @@ dependencies = [ "alloy-rpc-types-eth", "alloy-serde", "derive_more", - "op-alloy-consensus", + "op-alloy-consensus 0.22.4", "serde", "serde_json", "thiserror", @@ -3099,7 +4071,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", @@ -3125,6 +4097,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" @@ -3133,6 +4120,7 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -3159,7 +4147,7 @@ checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", - "http", + "http 1.4.0", "opentelemetry", ] @@ -3169,6 +4157,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[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" @@ -3215,11 +4209,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" @@ -3232,7 +4251,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] @@ -3316,13 +4335,19 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] +[[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" @@ -3347,6 +4372,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.114", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -3406,7 +4441,7 @@ checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", - "bitflags", + "bitflags 2.10.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -3426,6 +4461,21 @@ dependencies = [ "num-traits", ] +[[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" @@ -3530,11 +4580,20 @@ 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.2" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ec30b38a417407efe7676bad0ca6b78f995f810185ece9af3bd5dc561185a9" +checksum = "84816e4c99c467e92cf984ee6328caa976dfecd33a673544489d79ca2caaefe5" dependencies = [ "rustversion", ] @@ -3545,7 +4604,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cassowary", "compact_str", "crossterm", @@ -3560,13 +4619,72 @@ dependencies = [ "unicode-width 0.2.0", ] +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags 2.10.0", ] [[package]] @@ -3600,6 +4718,18 @@ 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.14" @@ -3611,6 +4741,12 @@ dependencies = [ "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.9" @@ -3623,13 +4759,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", @@ -3757,7 +4893,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3770,26 +4906,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 0.11.0", "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", + "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" @@ -3799,12 +4970,23 @@ 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", @@ -3830,9 +5012,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "schannel" @@ -3873,6 +5055,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" @@ -3906,17 +5098,30 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" dependencies = [ - "cc", + "cc", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] name = "security-framework" -version = "2.11.1" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags", - "core-foundation", + "bitflags 2.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4005,6 +5210,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" @@ -4023,7 +5239,7 @@ 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", @@ -4082,6 +5298,12 @@ dependencies = [ "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" @@ -4142,6 +5364,7 @@ dependencies = [ "cadence", "tokio", "tracing", + "tracing-subscriber", ] [[package]] @@ -4215,6 +5438,12 @@ dependencies = [ "time", ] +[[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.12" @@ -4230,6 +5459,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" @@ -4268,6 +5507,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.26.3" @@ -4341,9 +5603,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2379beea9476b89d0237078be761cf8e012d92d5ae4ae0c9a329f974838870fc" +checksum = "f8658017776544996edc21c8c7cc8bb4f13db60955382f4bac25dc6303b38438" dependencies = [ "paste", "proc-macro2", @@ -4379,17 +5641,55 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix 1.1.3", "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" @@ -4495,7 +5795,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -4521,13 +5821,23 @@ 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", + "rustls 0.23.36", "tokio", ] @@ -4543,6 +5853,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-tungstenite" version = "0.26.2" @@ -4552,11 +5877,11 @@ dependencies = [ "futures-util", "log", "native-tls", - "rustls", + "rustls 0.23.36", "rustls-pki-types", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.26.4", "tungstenite", "webpki-roots 0.26.11", ] @@ -4597,9 +5922,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" dependencies = [ "winnow", ] @@ -4625,11 +5950,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", @@ -4751,12 +6076,12 @@ checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" dependencies = [ "bytes", "data-encoding", - "http", + "http 1.4.0", "httparse", "log", "native-tls", "rand 0.9.2", - "rustls", + "rustls 0.23.36", "rustls-pki-types", "sha1", "thiserror", @@ -4795,9 +6120,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-segmentation" @@ -4859,6 +6184,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 = "utf-8" version = "0.7.6" @@ -4872,10 +6203,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "utf8parse" -version = "0.2.2" +name = "utils" +version = "0.0.0" +dependencies = [ + "alloy-rpc-types", + "metrics-exporter-prometheus", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +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" @@ -4895,6 +6244,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" @@ -4928,6 +6283,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -4987,6 +6351,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver 1.0.27", +] + [[package]] name = "wasmtimer" version = "0.4.3" @@ -5126,6 +6524,15 @@ 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" @@ -5162,6 +6569,21 @@ 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" @@ -5195,6 +6617,12 @@ dependencies = [ "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" @@ -5207,6 +6635,12 @@ 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" @@ -5219,6 +6653,12 @@ 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" @@ -5243,6 +6683,12 @@ 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" @@ -5255,6 +6701,12 @@ 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" @@ -5267,6 +6719,12 @@ 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" @@ -5279,6 +6737,12 @@ 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" @@ -5305,6 +6769,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -5357,6 +6903,22 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.3", +] + +[[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" @@ -5476,9 +7038,20 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.19" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "bindgen", + "cc", + "pkg-config", +] [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index c102d3e..2684429 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,19 @@ -[workspace] -resolver = "2" -members = ["crates/*", "bin/*"] -default-members = ["bin/*"] -exclude = [".github/"] + [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/*", "bin/*"] +default-members = ["bin/*"] +exclude = [".github/"] [workspace.lints.rust] missing-debug-implementations = "warn" @@ -44,42 +50,36 @@ needless-pass-by-ref-mut = "warn" string-lit-as-bytes = "warn" [workspace.dependencies] -cadence = "1.4" -clap = { version = "4.0", features = ["derive", "env"] } -tokio-tungstenite = { version = "0.26", features = ["native-tls"] } -futures-util = "0.3" -url = "2.5" -ratatui = "0.29" -crossterm = "0.28" -chrono = "0.4" -anyhow = "1.0" -serde_yaml = "0.9" -dirs = "6.0" -arboard = "3.4" -dotenvy = "0.15.7" -async-trait = "0.1" -mempool-rebroadcaster = { path = "./crates/mempool-rebroadcaster" } -sidecrush = { path = "./crates/sidecrush" } -metrics = "0.24.1" -metrics-derive = "0.1" -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" +# Internal crates +audit = { path = "crates/audit" } +basectl-cli = { path = "crates/basectl" } +mempool-rebroadcaster = { path = "crates/mempool-rebroadcaster" } +sidecrush = { path = "crates/sidecrush" } +utils = { path = "crates/utils" } + +# base-reth +base-bundles = { git = "https://github.com/base/base", branch = "main" } +base-primitives = { git = "https://github.com/base/base.git", features = ["flashblocks"] } +base-reth-rpc-types = { 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-trie = { version = "0.9.1", default-features = false } +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.5.2", features = ["ws", "pubsub"] } +alloy-consensus = { version = "1.0.41", 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-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-hardforks = { version = "0.5" } alloy-rpc-client = { version = "1.5.2" } alloy-transport-http = { version = "1.5.2" } @@ -87,11 +87,54 @@ alloy-sol-types = { version = "1.5.2" } alloy-contract = { version = "1.5.2" } # 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 } -# base -base-flashtypes = { git = "https://github.com/base/base.git" } -basectl-cli = { path = "crates/basectl" } +# tokio +tokio = { version = "1.47.1", features = ["full"] } + +# async +async-trait = "0.1.89" +futures = { version = "0.3.31", default-features = false } +futures-util = "0.3" + +# 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 +tokio-tungstenite = { version = "0.26", features = ["native-tls"] } +url = { version = "2.5.7", default-features = false } +axum = { version = "0.8.3", default-features = false } +ratatui = "0.29" +crossterm = "0.28" +clap = { version = "4.5.47", default-features = false, features = ["std", "derive", "env"] } +chrono = "0.4" +anyhow = { version = "1.0.99", default-features = false } +serde = { version = "1.0.219", default-features = false, features = ["derive"] } +serde_json = { version = "1.0.143", default-features = false } +serde_yaml = "0.9" +uuid = { version = "1.18.1", default-features = false } +backon = { version = "1.5.2", default-features = false } +dirs = "6.0" +arboard = "3.4" +dotenvy = { version = "0.15.7", default-features = false } +tracing = { version = "0.1.41", default-features = false } +tracing-subscriber = { version = "0.3.20", default-features = false, features = ["env-filter", "fmt", "ansi", "json"] } +wiremock = { version = "0.6.2", default-features = false } +metrics = { version = "0.24.1", default-features = false } +metrics-derive = { version = "0.1", default-features = false } +metrics-exporter-prometheus = { version = "0.17.0", default-features = false } +testcontainers = { version = "0.23.1", default-features = false } +testcontainers-modules = { version = "0.11.2", default-features = false } +moka = { version = "0.12.12", default-features = false } +cadence = "1.4" diff --git a/bin/audit/Cargo.toml b/bin/audit/Cargo.toml new file mode 100644 index 0000000..cf87213 --- /dev/null +++ b/bin/audit/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "audit-bin" +version.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +edition.workspace = true + +[lints] +workspace = true + +[[bin]] +name = "audit" +path = "src/main.rs" + +[dependencies] +utils.workspace = true +audit.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/audit/src/main.rs b/bin/audit/src/main.rs new file mode 100644 index 0000000..0e6dac4 --- /dev/null +++ b/bin/audit/src/main.rs @@ -0,0 +1,135 @@ +use std::net::SocketAddr; + +use anyhow::Result; +use audit::{KafkaAuditArchiver, KafkaAuditLogReader, S3EventReaderWriter, create_kafka_consumer}; +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 tracing::info; +use utils::{logger::init_logger_with_format, metrics::init_prometheus_exporter}; + +#[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: utils::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/audit/Cargo.toml b/crates/audit/Cargo.toml new file mode 100644 index 0000000..08c96d3 --- /dev/null +++ b/crates/audit/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "audit" +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 +base-bundles.workspace = true +utils = { workspace = true } +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-provider = { workspace = true } +alloy-primitives = { workspace = true, features = ["map-foldhash", "serde"] } +aws-sdk-s3 = { workspace = true, features = ["rustls", "default-https-client", "rt-tokio"] } +futures = { workspace = true } +alloy-signer-local = { workspace = true } +op-alloy-consensus = { workspace = true } +op-alloy-rpc-types = { workspace = true } + +[dev-dependencies] +audit = { workspace = true } +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/audit/src/archiver.rs b/crates/audit/src/archiver.rs new file mode 100644 index 0000000..d622094 --- /dev/null +++ b/crates/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/audit/src/lib.rs b/crates/audit/src/lib.rs new file mode 100644 index 0000000..060ded8 --- /dev/null +++ b/crates/audit/src/lib.rs @@ -0,0 +1,76 @@ +//! 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, +}; + +pub mod test_utils; + +use tokio::sync::mpsc; +use tracing::error; + +mod types; +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/audit/src/metrics.rs b/crates/audit/src/metrics.rs new file mode 100644 index 0000000..257b879 --- /dev/null +++ b/crates/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/audit/src/publisher.rs b/crates/audit/src/publisher.rs new file mode 100644 index 0000000..b845f14 --- /dev/null +++ b/crates/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/audit/src/reader.rs b/crates/audit/src/reader.rs new file mode 100644 index 0000000..f447a96 --- /dev/null +++ b/crates/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 tokio::time::sleep; +use tracing::{debug, error, info}; +use utils::kafka::load_kafka_config_from_file; + +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/audit/src/storage.rs b/crates/audit/src/storage.rs new file mode 100644 index 0000000..3c15ce1 --- /dev/null +++ b/crates/audit/src/storage.rs @@ -0,0 +1,925 @@ +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 base_bundles::AcceptedBundle; +use futures::future; +use serde::{Deserialize, Serialize}; +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 base_bundles::BundleExtensions; + use uuid::Uuid; + + use super::*; + use crate::{ + reader::Event, + test_utils::create_bundle_from_txn_data, + 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/audit/src/test_utils.rs b/crates/audit/src/test_utils.rs new file mode 100644 index 0000000..5a34e48 --- /dev/null +++ b/crates/audit/src/test_utils.rs @@ -0,0 +1,76 @@ +//! Test utilities for creating bundles and transactions. + +use alloy_consensus::SignableTransaction; +use alloy_primitives::{Address, Bytes, TxHash, U256, b256, bytes}; +use alloy_provider::network::{TxSignerSync, eip2718::Encodable2718}; +use alloy_signer_local::PrivateKeySigner; +use base_bundles::{AcceptedBundle, Bundle, MeterBundleResponse}; +use op_alloy_consensus::OpTxEnvelope; +use op_alloy_rpc_types::OpTransactionRequest; + +// 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: alloy_primitives::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/crates/audit/src/types.rs b/crates/audit/src/types.rs new file mode 100644 index 0000000..96adb18 --- /dev/null +++ b/crates/audit/src/types.rs @@ -0,0 +1,318 @@ +use alloy_consensus::transaction::{SignerRecoverable, Transaction as ConsensusTransaction}; +use alloy_primitives::{Address, B256, TxHash, U256}; +use base_bundles::AcceptedBundle; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +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/audit/tests/common/mod.rs b/crates/audit/tests/common/mod.rs new file mode 100644 index 0000000..095689c --- /dev/null +++ b/crates/audit/tests/common/mod.rs @@ -0,0 +1,75 @@ +use audit::test_utils::create_bundle_from_txn_data; +use base_bundles::BundleExtensions; +use rdkafka::{ClientConfig, consumer::StreamConsumer, producer::FutureProducer}; +use testcontainers::runners::AsyncRunner; +use testcontainers_modules::{kafka, kafka::Kafka, minio::MinIO}; +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/audit/tests/integration_tests.rs b/crates/audit/tests/integration_tests.rs new file mode 100644 index 0000000..90a1c4b --- /dev/null +++ b/crates/audit/tests/integration_tests.rs @@ -0,0 +1,117 @@ +#![allow(missing_docs)] + +use std::time::Duration; + +use alloy_primitives::{Address, B256, U256}; +use audit::{ + BundleEvent, BundleEventPublisher, BundleEventS3Reader, DropReason, KafkaAuditArchiver, + KafkaAuditLogReader, KafkaBundleEventPublisher, KafkaUserOpAuditLogReader, + KafkaUserOpEventPublisher, S3EventReaderWriter, UserOpEvent, UserOpEventPublisher, + UserOpEventReader, test_utils::create_bundle_from_txn_data, +}; +use base_bundles::BundleExtensions; +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/audit/tests/s3_test.rs b/crates/audit/tests/s3_test.rs new file mode 100644 index 0000000..6ffc979 --- /dev/null +++ b/crates/audit/tests/s3_test.rs @@ -0,0 +1,395 @@ +#![allow(missing_docs)] + +use std::sync::Arc; + +use alloy_primitives::{Address, B256, TxHash, U256}; +use audit::{ + BundleEvent, BundleEventS3Reader, Event, EventWriter, S3EventReaderWriter, UserOpDropReason, + UserOpEvent, UserOpEventS3Reader, UserOpEventWrapper, UserOpEventWriter, + test_utils::{TXN_HASH, create_bundle_from_txn_data}, +}; +use base_bundles::BundleExtensions; +use tokio::task::JoinSet; +use uuid::Uuid; + +mod common; +use common::TestHarness; + +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/basectl/Cargo.toml b/crates/basectl/Cargo.toml index 3064f76..f12ca94 100644 --- a/crates/basectl/Cargo.toml +++ b/crates/basectl/Cargo.toml @@ -24,7 +24,7 @@ alloy-rpc-types-eth = { workspace = true } alloy-eips = { workspace = true } op-alloy-network = { workspace = true } op-alloy-consensus = { workspace = true } -base-flashtypes = { workspace = true } +base-primitives = { workspace = true } url = { workspace = true } alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } diff --git a/crates/basectl/src/app/resources.rs b/crates/basectl/src/app/resources.rs index ac74277..55696a1 100644 --- a/crates/basectl/src/app/resources.rs +++ b/crates/basectl/src/app/resources.rs @@ -1,6 +1,6 @@ use std::collections::VecDeque; -use base_flashtypes::Flashblock; +use base_primitives::flashblocks::Flashblock; use tokio::sync::mpsc; use crate::{ diff --git a/crates/basectl/src/app/runner.rs b/crates/basectl/src/app/runner.rs index a596675..07a6359 100644 --- a/crates/basectl/src/app/runner.rs +++ b/crates/basectl/src/app/runner.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use base_flashtypes::Flashblock; +use base_primitives::flashblocks::Flashblock; use tokio::sync::mpsc; use super::{App, Resources, ViewId, views::create_view}; diff --git a/crates/basectl/src/rpc.rs b/crates/basectl/src/rpc.rs index 2957a4d..41c4713 100644 --- a/crates/basectl/src/rpc.rs +++ b/crates/basectl/src/rpc.rs @@ -5,7 +5,7 @@ use alloy_primitives::{Address, B256}; use alloy_provider::{Provider, ProviderBuilder}; use alloy_rpc_types_eth::{BlockNumberOrTag, TransactionTrait}; use anyhow::Result; -use base_flashtypes::Flashblock; +use base_primitives::flashblocks::Flashblock; use futures_util::{StreamExt, stream}; use op_alloy_network::Optimism; use tokio::sync::mpsc; diff --git a/crates/mempool-rebroadcaster/Cargo.toml b/crates/mempool-rebroadcaster/Cargo.toml index c562f63..e2d232a 100644 --- a/crates/mempool-rebroadcaster/Cargo.toml +++ b/crates/mempool-rebroadcaster/Cargo.toml @@ -14,7 +14,8 @@ path = "src/lib.rs" serde = { workspace = true } serde_json = { workspace = true } tracing = { workspace = true } -tokio = { workspace = true } +tracing-subscriber = { workspace = true, features = ["fmt", "std", "json", "env-filter"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } # alloy alloy-primitives.workspace = true @@ -23,4 +24,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 10d8d15..89f6564 100644 --- a/crates/sidecrush/Cargo.toml +++ b/crates/sidecrush/Cargo.toml @@ -14,16 +14,14 @@ path = "src/lib.rs" anyhow = { workspace = true } async-trait = { workspace = true } cadence = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] } tracing = { workspace = true } -tokio = { workspace = true } +tracing-subscriber = { workspace = true, features = ["fmt", "std", "json"] } # Ethereum client deps (will be used for header fetching) -alloy-provider = { workspace = true } -alloy-transport-http = { workspace = true } -alloy-rpc-types-eth = { workspace = true } alloy-consensus = { workspace = true } alloy-primitives = { workspace = true } +alloy-provider = { workspace = true, features = ["reqwest"] } +alloy-rpc-types-eth = { workspace = true } +alloy-transport-http = { workspace = true } -[dev-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -tracing = { workspace = true } diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml new file mode 100644 index 0000000..e875ce3 --- /dev/null +++ b/crates/utils/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "utils" +description = "Shared utility functions for logging, metrics, and config parsing" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +tracing = { workspace = true, features = ["std"] } +alloy-rpc-types = { workspace = true, features = ["eth"] } +metrics-exporter-prometheus = { workspace = true, features = ["http-listener"] } +tracing-subscriber = { workspace = true, features = ["std", "fmt", "ansi", "env-filter", "json"] } + +[dev-dependencies] +serde_json = { workspace = true, features = ["std"] } diff --git a/crates/utils/README.md b/crates/utils/README.md new file mode 100644 index 0000000..546ed46 --- /dev/null +++ b/crates/utils/README.md @@ -0,0 +1,3 @@ +# `utils` + +Shared utility functions for `base/infra` services — logging, metrics, and config parsing. diff --git a/crates/utils/src/kafka.rs b/crates/utils/src/kafka.rs new file mode 100644 index 0000000..ac07051 --- /dev/null +++ b/crates/utils/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/utils/src/lib.rs b/crates/utils/src/lib.rs new file mode 100644 index 0000000..a6eacd8 --- /dev/null +++ b/crates/utils/src/lib.rs @@ -0,0 +1,11 @@ +#![doc = include_str!("../README.md")] +#![doc(issue_tracker_base_url = "https://github.com/base/infra/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; diff --git a/crates/utils/src/logger.rs b/crates/utils/src/logger.rs new file mode 100644 index 0000000..ccdb8d2 --- /dev/null +++ b/crates/utils/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/utils/src/metrics.rs b/crates/utils/src/metrics.rs new file mode 100644 index 0000000..bb5f67f --- /dev/null +++ b/crates/utils/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/deny.toml b/deny.toml index 5c4adf5..3b760be 100644 --- a/deny.toml +++ b/deny.toml @@ -48,11 +48,38 @@ skip = [ "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", + # Proc-macro / derive ecosystem (alloy-tx-macros vs ratatui/instability) "darling", "darling_core", diff --git a/justfile b/justfile index 43fcb90..c578548 100644 --- a/justfile +++ b/justfile @@ -102,6 +102,6 @@ docker-image: run-mempool-rebroadcaster: cargo run --bin mempool-rebroadcaster -- --geth-mempool-endpoint {{env_var("GETH_MEMPOOL_ENDPOINT")}} --reth-mempool-endpoint {{env_var("RETH_MEMPOOL_ENDPOINT")}} -# Run basectl with specified config (mainnet, sepolia, devnet, or path) -basectl config="mainnet": - cargo run -p basectl --release -- -c {{config}} +# Run tips audit service +run-audit: + cargo run --bin audit