From 1430acca38ac55f0334c71b3a91671df69f106f7 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:47:30 +0100 Subject: [PATCH 01/11] feat: add deadline.rs --- Cargo.lock | 669 ++++++++++++++++++------------------ crates/core/Cargo.toml | 13 +- crates/core/src/deadline.rs | 563 ++++++++++++++++++++++++++++++ crates/core/src/lib.rs | 3 + 4 files changed, 918 insertions(+), 330 deletions(-) create mode 100644 crates/core/src/deadline.rs diff --git a/Cargo.lock b/Cargo.lock index aace3e87..7373e0d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,9 +75,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4973038846323e4e69a433916522195dce2947770076c03078fc21c80ea0f1c4" +checksum = "07dc44b606f29348ce7c127e7f872a6d2df3cfeff85b7d6bba62faca75112fdd" dependencies = [ "alloy-consensus", "alloy-contract", @@ -98,9 +98,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.2.30" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f374d3c6d729268bbe2d0e0ff992bb97898b2df756691a62ee1d5f0506bc39" +checksum = "6d9d22005bf31b018f31ef9ecadb5d2c39cf4f6acc8db0456f72c815f3d7f757" dependencies = [ "alloy-primitives", "num_enum", @@ -109,9 +109,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c0dc44157867da82c469c13186015b86abef209bf0e41625e4b68bac61d728" +checksum = "4e4ff99651d46cef43767b5e8262ea228cd05287409ccb0c947cc25e70a952f9" dependencies = [ "alloy-eips", "alloy-primitives", @@ -136,9 +136,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4cdb42df3871cd6b346d6a938ec2ba69a9a0f49d1f82714bc5c48349268434" +checksum = "1a0701b0eda8051a2398591113e7862f807ccdd3315d0b441f06c2a0865a379b" dependencies = [ "alloy-consensus", "alloy-eips", @@ -150,9 +150,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca63b7125a981415898ffe2a2a696c83696c9c6bdb1671c8a912946bbd8e49e7" +checksum = "f3c83c7a3c4e1151e8cac383d0a67ddf358f37e5ea51c95a1283d897c9de0a5a" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -239,9 +239,9 @@ dependencies = [ [[package]] name = "alloy-eip7928" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3231de68d5d6e75332b7489cfcc7f4dfabeba94d990a10e4b923af0e6623540" +checksum = "f8222b1d88f9a6d03be84b0f5e76bb60cd83991b43ad8ab6477f0e4a7809b98d" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -251,9 +251,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f7ef09f21bd1e9cb8a686f168cb4a206646804567f0889eadb8dcc4c9288c8" +checksum = "def1626eea28d48c6cc0a6f16f34d4af0001906e4f889df6c660b39c86fd044d" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -275,9 +275,9 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9cf3b99f46615fbf7dc1add0c96553abb7bf88fc9ec70dfbe7ad0b47ba7fe8" +checksum = "55d9d1aba3f914f0e8db9e4616ae37f3d811426d95bdccf44e47d0605ab202f6" dependencies = [ "alloy-eips", "alloy-primitives", @@ -302,9 +302,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff42cd777eea61f370c0b10f2648a1c81e0b783066cd7269228aa993afd487f7" +checksum = "e57586581f2008933241d16c3e3f633168b3a5d2738c5c42ea5246ec5e0ef17a" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -317,9 +317,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cbca04f9b410fdc51aaaf88433cbac761213905a65fe832058bcf6690585762" +checksum = "3b36c2a0ed74e48851f78415ca5b465211bd678891ba11e88fee09eac534bab1" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -343,9 +343,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d6d15e069a8b11f56bef2eccbad2a873c6dd4d4c81d04dda29710f5ea52f04" +checksum = "636c8051da58802e757b76c3b65af610b95799f72423dc955737dec73de234fd" dependencies = [ "alloy-consensus", "alloy-eips", @@ -383,9 +383,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d181c8cc7cf4805d7e589bf4074d56d55064fa1a979f005a45a62b047616d870" +checksum = "b3dd56e2eafe8b1803e325867ac2c8a4c73c9fb5f341ffd8347f9344458c5922" dependencies = [ "alloy-chains", "alloy-consensus", @@ -439,14 +439,14 @@ checksum = "ce8849c74c9ca0f5a03da1c865e3eb6f768df816e67dd3721a398a8a7e398011" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "alloy-rpc-client" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2792758a93ae32a32e9047c843d536e1448044f78422d71bf7d7c05149e103f" +checksum = "91577235d341a1bdbee30a463655d08504408a4d51e9f72edbfc5a622829f402" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -467,9 +467,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bdcbf9dfd5eea8bfeb078b1d906da8cd3a39c4d4dbe7a628025648e323611f6" +checksum = "79cff039bf01a17d76c0aace3a3a773d5f895eb4c68baaae729ec9da9e86c99c" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -479,9 +479,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd720b63f82b457610f2eaaf1f32edf44efffe03ae25d537632e7d23e7929e1a" +checksum = "73234a141ecce14e2989748c04fcac23deee67a445e2c4c167cfb42d4dacd1b6" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -490,9 +490,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2dc411f13092f237d2bf6918caf80977fc2f51485f9b90cb2a2f956912c8c9" +checksum = "010e101dbebe0c678248907a2545b574a87d078d82c2f6f5d0e8e7c9a6149a10" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -511,9 +511,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2ce1e0dbf7720eee747700e300c99aac01b1a95bb93f493a01e78ee28bb1a37" +checksum = "9e6d631f8b975229361d8af7b2c749af31c73b3cf1352f90e144ddb06227105e" dependencies = [ "alloy-primitives", "serde", @@ -522,9 +522,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2425c6f314522c78e8198979c8cbf6769362be4da381d4152ea8eefce383535d" +checksum = "97f40010b5e8f79b70bf163b38cd15f529b18ca88c4427c0e43441ee54e4ed82" dependencies = [ "alloy-primitives", "async-trait", @@ -537,9 +537,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ecb71ee53d8d9c3fa7bac17542c8116ebc7a9726c91b1bf333ec3d04f5a789" +checksum = "9c4ec1cc27473819399a3f0da83bc1cef0ceaac8c1c93997696e46dc74377a58" dependencies = [ "alloy-consensus", "alloy-network", @@ -562,7 +562,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -580,7 +580,7 @@ dependencies = [ "proc-macro2", "quote", "sha3", - "syn 2.0.116", + "syn 2.0.117", "syn-solidity", ] @@ -598,7 +598,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.116", + "syn 2.0.117", "syn-solidity", ] @@ -626,9 +626,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa186e560d523d196580c48bf00f1bf62e63041f28ecf276acc22f8b27bb9f53" +checksum = "a03bb3f02b9a7ab23dacd1822fa7f69aa5c8eefcdcf57fad085e0b8d76fb4334" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -649,9 +649,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa501ad58dd20acddbfebc65b52e60f05ebf97c52fa40d1b35e91f5e2da0ad0e" +checksum = "5ce599598ef8ebe067f3627509358d9faaa1ef94f77f834a7783cd44209ef55c" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -682,14 +682,14 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.7.3" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa0c53e8c1e1ef4d01066b01c737fb62fc9397ab52c6e7bb5669f97d281b9bc" +checksum = "397406cf04b11ca2a48e6f81804c70af3f40a36abf648e11dc7416043eb0834d" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -759,9 +759,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "ark-ff" @@ -848,7 +848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -886,7 +886,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -991,7 +991,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -1003,7 +1003,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1087,7 +1087,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1098,7 +1098,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1140,7 +1140,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1151,9 +1151,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" dependencies = [ "aws-lc-sys", "zeroize", @@ -1161,9 +1161,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" dependencies = [ "cc", "cmake", @@ -1282,7 +1282,7 @@ checksum = "7b9a5040dce49a7642c97ccb1ae59567098967b5d52c29773f1299a42d23bb39" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1322,12 +1322,6 @@ 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.11.0" @@ -1384,7 +1378,7 @@ checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b" dependencies = [ "async-stream", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags", "bollard-buildkit-proto", "bollard-stubs", "bytes", @@ -1475,7 +1469,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1498,7 +1492,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1523,9 +1517,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byte-slice-cast" @@ -1550,9 +1544,9 @@ dependencies = [ [[package]] name = "c-kzg" -version = "2.1.5" +version = "2.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" +checksum = "1a0f582957c24870b7bfd12bf562c40b4734b533cafbaf8ded31d6d85f462c01" dependencies = [ "blst", "cc", @@ -1652,9 +1646,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -1704,9 +1698,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.59" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -1714,9 +1708,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.59" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -1733,7 +1727,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1784,9 +1778,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" +checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" dependencies = [ "cfg-if", "cpufeatures", @@ -2027,7 +2021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2074,7 +2068,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2118,7 +2112,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2133,7 +2127,7 @@ dependencies = [ "quote", "serde", "strsim", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2146,7 +2140,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2157,7 +2151,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2168,7 +2162,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2179,7 +2173,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2219,7 +2213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2267,9 +2261,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -2305,7 +2299,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.116", + "syn 2.0.117", "unicode-xid", ] @@ -2338,7 +2332,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2418,7 +2412,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2478,7 +2472,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2498,7 +2492,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2818,7 +2812,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2921,20 +2915,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -2955,7 +2949,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.11.0", + "bitflags", "libc", "libgit2-sys", "log", @@ -3353,8 +3347,8 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", - "system-configuration 0.7.0", + "socket2 0.6.3", + "system-configuration", "tokio", "tower-service", "tracing", @@ -3516,19 +3510,19 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.10.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabb0019d51a643781ff15c9c8a3e5dedc365c47211270f4e8f82812fedd8f0a" +checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] name = "if-watch" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf9d64cfcf380606e64f9a0bcf493616b65331199f984151a6fa11a7b3cde38" +checksum = "71c02a5161c313f0cbdbadc511611893584a10a7b6153cb554bdf83ddce99ec2" dependencies = [ "async-io", "core-foundation 0.9.4", @@ -3542,9 +3536,9 @@ dependencies = [ "netlink-proto", "netlink-sys", "rtnetlink", - "system-configuration 0.6.1", + "system-configuration", "tokio", - "windows 0.53.0", + "windows 0.62.2", ] [[package]] @@ -3585,7 +3579,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3634,9 +3628,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -3721,9 +3715,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -3777,9 +3771,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libgit2-sys" @@ -4328,7 +4322,7 @@ checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" dependencies = [ "heck", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4342,7 +4336,7 @@ dependencies = [ "if-watch", "libc", "libp2p-core", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", "tracing", ] @@ -4507,25 +4501,26 @@ dependencies = [ "thiserror 2.0.18", "tracing", "yamux 0.12.1", - "yamux 0.13.8", + "yamux 0.13.9", ] [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.11.0", + "bitflags", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" dependencies = [ "cc", "libc", @@ -4535,9 +4530,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -4593,7 +4588,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4604,7 +4599,7 @@ checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4673,9 +4668,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.13" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -4769,46 +4764,30 @@ dependencies = [ [[package]] name = "netlink-packet-core" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" dependencies = [ - "anyhow", - "byteorder", - "netlink-packet-utils", + "paste", ] [[package]] name = "netlink-packet-route" -version = "0.17.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" dependencies = [ - "anyhow", - "bitflags 1.3.2", - "byteorder", + "bitflags", "libc", + "log", "netlink-packet-core", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-utils" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" -dependencies = [ - "anyhow", - "byteorder", - "paste", - "thiserror 1.0.69", ] [[package]] name = "netlink-proto" -version = "0.11.5" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" dependencies = [ "bytes", "futures", @@ -4833,12 +4812,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.4" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 1.3.2", + "bitflags", "cfg-if", + "cfg_aliases", "libc", ] @@ -4984,7 +4964,7 @@ checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5044,7 +5024,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_with", - "syn 2.0.116", + "syn 2.0.117", "thiserror 2.0.18", "uuid", "validator", @@ -5093,7 +5073,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -5110,7 +5090,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5178,7 +5158,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5232,7 +5212,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5310,29 +5290,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -5356,6 +5336,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plotters" version = "0.3.7" @@ -5492,17 +5478,22 @@ dependencies = [ "cancellation", "chrono", "crossbeam", + "futures", "hex", "libp2p", "pluto-build-proto", + "pluto-eth2api", "prost 0.14.3", "prost-types 0.14.3", "rand 0.8.5", "regex", "serde", "serde_json", + "test-case", "thiserror 2.0.18", "tokio", + "tokio-util", + "tracing", ] [[package]] @@ -5795,7 +5786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5820,11 +5811,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.4+spec-1.1.0", ] [[package]] @@ -5846,7 +5837,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5878,7 +5869,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5889,7 +5880,7 @@ checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.11.0", + "bitflags", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -5935,7 +5926,7 @@ dependencies = [ "prost 0.14.3", "prost-types 0.14.3", "regex", - "syn 2.0.116", + "syn 2.0.117", "tempfile", ] @@ -5949,7 +5940,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5962,7 +5953,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6013,9 +6004,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.1" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd58c6a1fc307e1092aa0bb23d204ca4d1f021764142cd0424dccc84d2d5d106" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "memchr", "serde", @@ -6035,7 +6026,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.2", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -6044,9 +6035,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", @@ -6073,16 +6064,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -6093,6 +6084,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -6213,9 +6210,9 @@ dependencies = [ [[package]] name = "rapidhash" -version = "4.4.0" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111325c42c4bafae99e777cd77b40dea9a2b30c69e9d8c74b6eccd7fba4337de" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" dependencies = [ "rustversion", ] @@ -6259,16 +6256,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] @@ -6288,7 +6285,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6316,9 +6313,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -6448,15 +6445,15 @@ dependencies = [ [[package]] name = "rtnetlink" -version = "0.13.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0" +checksum = "4b960d5d873a75b5be9761b1e73b146f52dddcd27bac75263f40fba686d4d7b5" dependencies = [ - "futures", + "futures-channel", + "futures-util", "log", "netlink-packet-core", "netlink-packet-route", - "netlink-packet-utils", "netlink-proto", "netlink-sys", "nix", @@ -6539,11 +6536,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -6552,9 +6549,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -6778,11 +6775,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", + "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -6791,9 +6788,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -6869,7 +6866,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6905,7 +6902,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6931,9 +6928,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "base64 0.22.1", "chrono", @@ -6950,14 +6947,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7110,12 +7107,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7170,7 +7167,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7181,7 +7178,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7202,7 +7199,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7224,9 +7221,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -7242,7 +7239,7 @@ dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7262,7 +7259,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7279,24 +7276,13 @@ dependencies = [ "windows 0.57.0", ] -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - [[package]] name = "system-configuration" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.0", + "bitflags", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -7325,12 +7311,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -7354,7 +7340,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7365,7 +7351,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "test-case-core", ] @@ -7425,7 +7411,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7436,7 +7422,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7525,9 +7511,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -7535,20 +7521,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7619,9 +7605,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] @@ -7642,12 +7628,12 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap 2.13.0", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "winnow", ] @@ -7669,9 +7655,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tonic" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f32a6f80051a4111560201420c7885d0082ba9efe2ab61875c587bb6b18b9a0" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ "async-trait", "axum", @@ -7686,7 +7672,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2 0.6.2", + "socket2 0.6.3", "sync_wrapper", "tokio", "tokio-stream", @@ -7698,9 +7684,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f86539c0089bfd09b1f8c0ab0239d80392af74c21bc9e0f15e1b4aca4c1647f" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" dependencies = [ "bytes", "prost 0.14.3", @@ -7732,7 +7718,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.11.0", + "bitflags", "bytes", "futures-util", "http", @@ -7776,7 +7762,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7871,7 +7857,7 @@ dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8053,11 +8039,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -8090,7 +8076,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8145,7 +8131,7 @@ source = "git+https://github.com/matter-labs/vise?rev=73c654303d8190023cf30034d6 dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8208,9 +8194,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -8221,9 +8207,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -8235,9 +8221,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8245,22 +8231,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -8306,7 +8292,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags", "hashbrown 0.15.5", "indexmap 2.13.0", "semver 1.0.27", @@ -8328,9 +8314,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -8412,32 +8398,33 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.53.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ - "windows-core 0.53.0", + "windows-core 0.57.0", "windows-targets 0.52.6", ] [[package]] name = "windows" -version = "0.57.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", ] [[package]] -name = "windows-core" -version = "0.53.0" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcc5b895a6377f1ab9fa55acedab1fd5ac0db66ad1e6c7f47e28a22e446a5dd" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-core 0.62.2", ] [[package]] @@ -8465,6 +8452,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.57.0" @@ -8473,7 +8471,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8484,7 +8482,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8495,7 +8493,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8506,7 +8504,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8515,6 +8513,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -8661,6 +8669,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -8843,9 +8860,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -8913,7 +8930,7 @@ dependencies = [ "heck", "indexmap 2.13.0", "prettyplease", - "syn 2.0.116", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -8929,7 +8946,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -8941,7 +8958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags", "indexmap 2.13.0", "log", "serde", @@ -9057,9 +9074,9 @@ dependencies = [ [[package]] name = "yamux" -version = "0.13.8" +version = "0.13.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deab71f2e20691b4728b349c6cee8fc7223880fa67b6b4f92225ec32225447e5" +checksum = "c650efd29044140aa63caaf80129996a9e2659a2ab7045a7e061807d02fc8549" dependencies = [ "futures", "log", @@ -9099,28 +9116,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "96e13bc581734df6250836c59a5f44f3c57db9f9acb9dc8e3eaabdaf6170254d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "3545ea9e86d12ab9bba9fcd99b54c1556fd3199007def5a03c375623d05fac1c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -9140,7 +9157,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -9161,7 +9178,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -9194,7 +9211,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 924b1860..0d511157 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -10,15 +10,19 @@ publish.workspace = true cancellation.workspace = true chrono.workspace = true crossbeam.workspace = true +futures.workspace = true hex.workspace = true +libp2p.workspace = true +pluto-eth2api.workspace = true +prost.workspace = true +prost-types.workspace = true +regex.workspace = true serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio.workspace = true -libp2p.workspace = true -regex.workspace = true -prost.workspace = true -prost-types.workspace = true +tokio-util.workspace = true +tracing.workspace = true [dev-dependencies] rand.workspace = true @@ -27,6 +31,7 @@ prost.workspace = true prost-types.workspace = true hex.workspace = true chrono.workspace = true +test-case.workspace = true [build-dependencies] pluto-build-proto.workspace = true diff --git a/crates/core/src/deadline.rs b/crates/core/src/deadline.rs new file mode 100644 index 00000000..ac89c622 --- /dev/null +++ b/crates/core/src/deadline.rs @@ -0,0 +1,563 @@ +//! Duty deadline tracking and notification functionality. +//! +//! This module provides the [`Deadliner`] trait for tracking duty deadlines +//! and notifying when duties expire. It implements a background task that +//! manages timers for multiple duties and sends expired duties to a channel. +//! +//! # Example +//! +//! ```no_run +//! use chrono::{DateTime, Utc}; +//! use pluto_core::{ +//! deadline::{DeadlineFunc, new_deadliner}, +//! types::{Duty, DutyType, SlotNumber}, +//! }; +//! use std::sync::Arc; +//! use tokio_util::sync::CancellationToken; +//! +//! # async fn example() { +//! let cancel_token = CancellationToken::new(); +//! +//! // Define a deadline function +//! let deadline_func: DeadlineFunc = Arc::new(|_duty| { +//! let deadline = DateTime::from_timestamp(1000, 0).unwrap(); +//! Ok(Some(deadline)) +//! }); +//! +//! let deadliner = new_deadliner(cancel_token, "example", deadline_func); +//! +//! // Add a duty +//! let duty = Duty::new_attester_duty(SlotNumber::new(1)); +//! let added = deadliner.add(duty).await; +//! +//! // Receive expired duties +//! if let Some(mut rx) = deadliner.c() { +//! while let Some(expired_duty) = rx.recv().await { +//! println!("Duty expired: {}", expired_duty); +//! } +//! } +//! # } +//! ``` +use crate::types::{Duty, DutyType, SlotNumber}; +use chrono::{DateTime, Utc}; +use futures::future::BoxFuture; +use pluto_eth2api::{EthBeaconNodeApiClient, EthBeaconNodeApiClientError}; +use std::{ + collections::HashSet, + sync::{Arc, Mutex}, +}; +use futures::future::FutureExt; +use tokio_util::sync::CancellationToken; + +/// Fraction of slot duration to use as a margin for network delays. +const MARGIN_FACTOR: i32 = 12; + +/// Type alias for the deadline function. +/// +/// Takes a duty and returns an optional deadline. +/// Returns `Ok(Some(deadline))` if the duty expires at the given time. +/// Returns `Ok(None)` if the duty never expires. +pub type DeadlineFunc = Arc Result>> + Send + Sync>; + +/// Error types for deadline operations. +#[derive(Debug, thiserror::Error)] +pub enum DeadlineError { + /// Failed to fetch genesis time from beacon node. + #[error("Failed to fetch genesis time: {0}")] + FetchGenesisTime(#[from] EthBeaconNodeApiClientError), + + /// Deadliner has been shut down. + #[error("Deadliner has been shut down")] + Shutdown, + + /// Arithmetic overflow in deadline calculation. + #[error("Arithmetic overflow in deadline calculation")] + ArithmeticOverflow, + + /// Duration conversion failed. + #[error("Duration conversion failed")] + DurationConversion, + + /// DateTime calculation failed. + #[error("DateTime calculation failed")] + DateTimeCalculation, +} + +/// Result type for deadline operations. +pub type Result = std::result::Result; + +/// Converts a `std::time::Duration` to `chrono::Duration`. +fn to_chrono_duration(duration: std::time::Duration) -> Result { + chrono::Duration::from_std(duration).map_err(|_| DeadlineError::DurationConversion) +} + +/// Converts seconds (u64) to `chrono::Duration`. +fn secs_to_chrono(secs: u64) -> Result { + let secs_i64 = i64::try_from(secs).map_err(|_| DeadlineError::ArithmeticOverflow)?; + chrono::Duration::try_seconds(secs_i64).ok_or(DeadlineError::DurationConversion) +} + +/// Deadliner provides duty deadline functionality. +/// +/// The `c()` method returns a channel for receiving expired duties. +/// It may only be called once and the returned channel should be used +/// by a single task. Multiple instances are required for different +/// components and use cases. +pub trait Deadliner: Send + Sync { + /// Adds a duty for deadline scheduling. + /// + /// Returns `true` if the duty was added for future deadline scheduling. + /// This method is idempotent and returns `true` if the duty was previously + /// added and still awaits deadline scheduling. + /// + /// Returns `false` if: + /// - The duty has already expired and cannot be scheduled + /// - The duty never expires (e.g., Exit, BuilderRegistration) + fn add(&self, duty: Duty) -> BoxFuture<'_, bool>; + + /// Returns the channel for receiving deadlined duties. + /// + /// This method may only be called once and returns `None` on subsequent + /// calls. The returned channel should only be used by a single task. + fn c(&self) -> Option>; +} + +/// Creates a deadline function from the Ethereum 2.0 beacon node configuration. +/// +/// Fetches genesis time and slot duration from the beacon node and returns +/// a function that calculates deadlines for each duty type. +/// +/// # Errors +/// +/// Returns an error if fetching genesis time or slots config fails. +pub async fn new_duty_deadline_func(client: &EthBeaconNodeApiClient) -> Result { + let genesis_time = client.fetch_genesis_time().await?; + let (slot_duration, _slots_per_epoch) = client.fetch_slots_config().await?; + + // Convert std::time::Duration to chrono::Duration for slot_duration + let slot_duration = to_chrono_duration(slot_duration)?; + + Ok(Arc::new(move |duty: Duty| { + // Exit and BuilderRegistration duties never expire + match duty.duty_type { + DutyType::Exit | DutyType::BuilderRegistration => { + return Ok(None); + } + _ => {} + } + + // Calculate slot start time + // start = genesis_time + (slot * slot_duration) + let slot_secs = duty + .slot + .inner() + .checked_mul( + u64::try_from(slot_duration.num_seconds()) + .map_err(|_| DeadlineError::ArithmeticOverflow)?, + ) + .ok_or(DeadlineError::ArithmeticOverflow)?; + let slot_offset = secs_to_chrono(slot_secs)?; + + let start: DateTime = genesis_time + .checked_add_signed(slot_offset) + .ok_or(DeadlineError::DateTimeCalculation)?; + + // Calculate margin: slot_duration / MARGIN_FACTOR + let margin = slot_duration + .checked_div(MARGIN_FACTOR) + .ok_or(DeadlineError::ArithmeticOverflow)?; + + // Calculate duty-specific duration + let duration = match duty.duty_type { + DutyType::Proposer | DutyType::Randao => { + // duration = slot_duration / 3 + slot_duration + .checked_div(3) + .ok_or(DeadlineError::ArithmeticOverflow)? + } + DutyType::SyncMessage => { + // duration = 2 * slot_duration / 3 + slot_duration + .checked_mul(2) + .and_then(|s| s.checked_div(3)) + .ok_or(DeadlineError::ArithmeticOverflow)? + } + DutyType::Attester | DutyType::Aggregator | DutyType::PrepareAggregator => { + // duration = 2 * slot_duration + // Even though attestations and aggregations are acceptable after 2 slots, + // the rewards are heavily diminished. + slot_duration + .checked_mul(2) + .ok_or(DeadlineError::ArithmeticOverflow)? + } + _ => { + // Default: duration = slot_duration + slot_duration + } + }; + + // Calculate final deadline: start + duration + margin + let deadline = start + .checked_add_signed(duration) + .and_then(|t| t.checked_add_signed(margin)) + .ok_or(DeadlineError::DateTimeCalculation)?; + + Ok(Some(deadline)) + })) +} + +/// Gets the duty with the earliest deadline from the duties map. +/// +/// Returns a tuple of (duty, deadline). If no duties are available, +/// returns a sentinel far-future date (9999-01-01). +fn get_curr_duty(duties: &HashSet, deadline_func: &DeadlineFunc) -> (Duty, DateTime) { + let mut curr_duty = Duty::new(SlotNumber::new(0), DutyType::Unknown); + + // Use far-future sentinel date (9999-01-01) matching Go implementation + // This timestamp is a known constant and will never fail + let mut curr_deadline = + DateTime::from_timestamp(253402300799, 0).unwrap_or(DateTime::::MAX_UTC); + + for duty in duties.iter() { + let Ok(deadline_opt) = deadline_func(duty.clone()) else { + continue; + }; + + // Ignore duties that never expire + let Some(duty_deadline) = deadline_opt else { + continue; + }; + + // Update if this duty has an earlier deadline + if duty_deadline < curr_deadline { + curr_duty = duty.clone(); + curr_deadline = duty_deadline; + } + } + + (curr_duty, curr_deadline) +} + +/// Internal message type for adding duties to the deadliner. +struct DeadlineInput { + duty: Duty, + response_tx: tokio::sync::oneshot::Sender, +} + +/// Implementation of the Deadliner trait. +struct DeadlinerImpl { + cancel_token: CancellationToken, + input_tx: tokio::sync::mpsc::UnboundedSender, + output_rx: Arc>>>, +} + +impl Deadliner for DeadlinerImpl { + fn add(&self, duty: Duty) -> BoxFuture<'_, bool> { + Box::pin(async move { + // Check if shut down + if self.cancel_token.is_cancelled() { + return false; + } + + let (response_tx, response_rx) = tokio::sync::oneshot::channel(); + let input = DeadlineInput { duty, response_tx }; + + // Send the duty to the background task + if self.input_tx.send(input).is_err() { + return false; + } + + // Wait for response + response_rx.await.unwrap_or(false) + }) + } + + fn c(&self) -> Option> { + self.output_rx + .lock() + .ok() + .and_then(|mut guard| guard.take()) + } +} + +/// Clock trait for abstracting time operations. +trait Clock: Send + Sync { + /// Returns the current time. + fn now(&self) -> DateTime; + + /// Creates a sleep future that completes after the given duration. + fn sleep(&self, duration: std::time::Duration) -> BoxFuture<'static, ()>; +} + +/// Real clock implementation using tokio::time. +struct RealClock; + +impl Clock for RealClock { + fn now(&self) -> DateTime { + Utc::now() + } + + fn sleep(&self, duration: std::time::Duration) -> BoxFuture<'static, ()> { + tokio::time::sleep(duration).boxed() + } +} + +impl DeadlinerImpl { + /// Background task that manages duty deadlines. + /// + /// This is an associated function (not a method) because the DeadlinerImpl + /// is immediately wrapped in Arc, preventing mutable access. + async fn run_task( + cancel_token: CancellationToken, + label: String, + deadline_func: DeadlineFunc, + clock: Arc, + mut input_rx: tokio::sync::mpsc::UnboundedReceiver, + output_tx: tokio::sync::mpsc::Sender, + ) { + let mut duties: HashSet = HashSet::new(); + let (mut curr_duty, mut curr_deadline) = get_curr_duty(&duties, &deadline_func); + + // Create initial timer + let now = clock.now(); + let initial_duration = curr_deadline + .signed_duration_since(now) + .to_std() + .unwrap_or(std::time::Duration::ZERO); + let mut timer = clock.sleep(initial_duration); + + loop { + tokio::select! { + biased; + + _ = cancel_token.cancelled() => { + return; + } + + Some(input) = input_rx.recv() => { + let duty = input.duty; + let Ok(deadline_opt) = deadline_func(duty.clone()) else { + let _ = input.response_tx.send(false); + continue; + }; + + // Drop duties that never expire + let Some(deadline) = deadline_opt else { + let _ = input.response_tx.send(false); + continue; + }; + + let now = clock.now(); + let expired = deadline < now; + + let _ = input.response_tx.send(!expired); + + // Ignore expired duties + if expired { + continue; + } + + // Add duty to the map (idempotent) + duties.insert(duty); + + // Update timer if this deadline is earlier + if deadline < curr_deadline { + let (new_duty, new_deadline) = get_curr_duty(&duties, &deadline_func); + curr_duty = new_duty; + curr_deadline = new_deadline; + + let duration = curr_deadline + .signed_duration_since(clock.now()) + .to_std() + .unwrap_or(std::time::Duration::ZERO); + timer = clock.sleep(duration); + } + } + + _ = &mut timer => { + // Deadline expired - send duty to output channel + match output_tx.try_send(curr_duty.clone()) { + Ok(()) => {} + Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => { + tracing::warn!( + label = %label, + duty = %curr_duty, + "Deadliner output channel full" + ); + } + Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => { + return; + } + } + + // Remove duty from map + duties.remove(&curr_duty); + + // Update to next duty + let (new_duty, new_deadline) = get_curr_duty(&duties, &deadline_func); + curr_duty = new_duty; + curr_deadline = new_deadline; + + let duration = curr_deadline + .signed_duration_since(clock.now()) + .to_std() + .unwrap_or(std::time::Duration::ZERO); + timer = clock.sleep(duration); + } + } + } + } + + /// Internal constructor for creating a deadliner with a specific clock. + fn new_internal( + cancel_token: CancellationToken, + label: impl Into, + deadline_func: DeadlineFunc, + clock: Arc, + ) -> Arc { + const OUTPUT_BUFFER: usize = 10; + + let label = label.into(); + let (input_tx, input_rx) = tokio::sync::mpsc::unbounded_channel(); + let (output_tx, output_rx) = tokio::sync::mpsc::channel(OUTPUT_BUFFER); + + let impl_instance: Arc = Arc::new(DeadlinerImpl { + cancel_token: cancel_token.clone(), + input_tx, + output_rx: Arc::new(Mutex::new(Some(output_rx))), + }); + + // Spawn background task + tokio::spawn(Self::run_task( + cancel_token, + label, + deadline_func, + clock, + input_rx, + output_tx, + )); + + impl_instance + } +} + +/// Creates a new Deadliner instance. +/// +/// Starts a background task that manages duty deadlines and sends expired +/// duties to a channel. The background task runs until the cancellation token +/// is cancelled. +/// +/// # Arguments +/// +/// * `cancel_token` - Token to cancel the background task +/// * `label` - Label for logging purposes +/// * `deadline_func` - Function that calculates deadlines for duties +/// +/// # Returns +/// +/// An Arc-wrapped Deadliner trait object +pub fn new_deadliner( + cancel_token: CancellationToken, + label: impl Into, + deadline_func: DeadlineFunc, +) -> Arc { + DeadlinerImpl::new_internal(cancel_token, label, deadline_func, Arc::new(RealClock)) +} + +/// Creates a new Deadliner instance for testing with a fake clock. +/// +/// This constructor is intended for use in tests where you need to control +/// time progression. +/// +/// # Arguments +/// +/// * `cancel_token` - Token to cancel the background task +/// * `label` - Label for logging purposes +/// * `deadline_func` - Function that calculates deadlines for duties +/// * `clock` - Test clock for controlling time in tests +/// +/// # Returns +/// +/// An Arc-wrapped Deadliner trait object +#[cfg(test)] +fn new_deadliner_for_test( + cancel_token: CancellationToken, + label: impl Into, + deadline_func: DeadlineFunc, + clock: Arc, +) -> Arc { + DeadlinerImpl::new_internal(cancel_token, label, deadline_func, clock) +} + + +/// Fake clock implementation for testing. +#[cfg(test)] +type WakerList = Vec<(DateTime, std::task::Waker)>; + +#[cfg(test)] +struct TestClock { + start: std::sync::Arc>>, + wakers: std::sync::Arc>, +} + +#[cfg(test)] +impl TestClock { + fn new(start: DateTime) -> Self { + Self { + start: std::sync::Arc::new(std::sync::Mutex::new(start)), + wakers: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), + } + } + + fn advance(&self, duration: std::time::Duration) { + let new_time = { + let mut start = self.start.lock().unwrap(); + let chrono_duration = chrono::Duration::from_std(duration).unwrap(); + *start = start.checked_add_signed(chrono_duration).unwrap(); + *start + }; + + // Wake all timers that have expired + let mut wakers = self.wakers.lock().unwrap(); + let (expired, pending): (Vec<_>, Vec<_>) = wakers + .drain(..) + .partition(|(deadline, _)| *deadline <= new_time); + *wakers = pending; + + // Wake expired futures + for (_, waker) in expired { + waker.wake(); + } + } +} + +#[cfg(test)] +impl Clock for TestClock { + fn now(&self) -> DateTime { + *self.start.lock().unwrap() + } + + fn sleep(&self, duration: std::time::Duration) -> BoxFuture<'static, ()> { + let deadline = self + .now() + .checked_add_signed(chrono::Duration::from_std(duration).unwrap()) + .unwrap(); + let wakers = Arc::clone(&self.wakers); + let start = Arc::clone(&self.start); + + Box::pin(std::future::poll_fn(move |cx| { + let now = *start.lock().unwrap(); + if now >= deadline { + std::task::Poll::Ready(()) + } else { + // Register waker + let mut wakers = wakers.lock().unwrap(); + // Check if this waker is already registered for this deadline + if !wakers.iter().any(|(d, _)| *d == deadline) { + wakers.push((deadline, cx.waker().clone())); + } + std::task::Poll::Pending + } + })) + } +} \ No newline at end of file diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 104b8b1b..d34e1782 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -16,3 +16,6 @@ pub mod corepb; /// Semver version parsing utilities. pub mod version; + +/// Duty deadline tracking and notification. +pub mod deadline; From 364977ef687973733d08edfbb6fb6ceb8eda69e3 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:14:09 +0100 Subject: [PATCH 02/11] feat: deadline tests --- Cargo.lock | 1 + Cargo.toml | 1 + crates/core/Cargo.toml | 1 + crates/core/src/deadline.rs | 379 +++++++++++++++++++++++++++++++++++- 4 files changed, 377 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7373e0d1..c2e65f79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5474,6 +5474,7 @@ dependencies = [ name = "pluto-core" version = "1.7.1" dependencies = [ + "async-trait", "built", "cancellation", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 34b514cc..1f8ce1d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ license = "Apache-2.0" publish = false [workspace.dependencies] +async-trait = "0.1.89" alloy = { version = "1.3", features = ["essentials"] } built = { version = "0.8.0", features = ["git2", "chrono", "cargo-lock"] } blst = "0.3" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 0d511157..c1f74cbf 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true publish.workspace = true [dependencies] +async-trait.workspace = true cancellation.workspace = true chrono.workspace = true crossbeam.workspace = true diff --git a/crates/core/src/deadline.rs b/crates/core/src/deadline.rs index ac89c622..567113ea 100644 --- a/crates/core/src/deadline.rs +++ b/crates/core/src/deadline.rs @@ -39,14 +39,14 @@ //! # } //! ``` use crate::types::{Duty, DutyType, SlotNumber}; +use async_trait::async_trait; use chrono::{DateTime, Utc}; -use futures::future::BoxFuture; +use futures::future::{BoxFuture, FutureExt}; use pluto_eth2api::{EthBeaconNodeApiClient, EthBeaconNodeApiClientError}; use std::{ collections::HashSet, sync::{Arc, Mutex}, }; -use futures::future::FutureExt; use tokio_util::sync::CancellationToken; /// Fraction of slot duration to use as a margin for network delays. @@ -122,6 +122,35 @@ pub trait Deadliner: Send + Sync { fn c(&self) -> Option>; } +/// Trait for beacon clients that can provide genesis time and slot +/// configuration. +/// +/// This trait abstracts the necessary beacon node API calls for deadline +/// calculation. +#[async_trait] +pub trait BeaconClientForDeadline { + /// Fetches the genesis time from the beacon node. + async fn fetch_genesis_time(&self) -> Result>; + + /// Fetches the slot duration and slots per epoch from the beacon node. + async fn fetch_slots_config(&self) -> Result<(std::time::Duration, u64)>; +} + +#[async_trait] +impl BeaconClientForDeadline for EthBeaconNodeApiClient { + async fn fetch_genesis_time(&self) -> Result> { + self.fetch_genesis_time() + .await + .map_err(DeadlineError::FetchGenesisTime) + } + + async fn fetch_slots_config(&self) -> Result<(std::time::Duration, u64)> { + self.fetch_slots_config() + .await + .map_err(DeadlineError::FetchGenesisTime) + } +} + /// Creates a deadline function from the Ethereum 2.0 beacon node configuration. /// /// Fetches genesis time and slot duration from the beacon node and returns @@ -130,7 +159,9 @@ pub trait Deadliner: Send + Sync { /// # Errors /// /// Returns an error if fetching genesis time or slots config fails. -pub async fn new_duty_deadline_func(client: &EthBeaconNodeApiClient) -> Result { +pub async fn new_duty_deadline_func( + client: &C, +) -> Result { let genesis_time = client.fetch_genesis_time().await?; let (slot_duration, _slots_per_epoch) = client.fetch_slots_config().await?; @@ -489,7 +520,6 @@ fn new_deadliner_for_test( DeadlinerImpl::new_internal(cancel_token, label, deadline_func, clock) } - /// Fake clock implementation for testing. #[cfg(test)] type WakerList = Vec<(DateTime, std::task::Waker)>; @@ -560,4 +590,343 @@ impl Clock for TestClock { } })) } -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::SlotNumber; + use test_case::test_case; + + /// Helper function to create expired duties, non-expired duties, and + /// voluntary exits. + fn setup_data() -> (Vec, Vec, Vec) { + let expired_duties = vec![ + Duty::new_attester_duty(SlotNumber::new(1)), + Duty::new_proposer_duty(SlotNumber::new(2)), + Duty::new_randao_duty(SlotNumber::new(3)), + ]; + + let non_expired_duties = vec![ + Duty::new_proposer_duty(SlotNumber::new(1)), + Duty::new_attester_duty(SlotNumber::new(2)), + ]; + + let voluntary_exits = vec![ + Duty::new_voluntary_exit_duty(SlotNumber::new(2)), + Duty::new_voluntary_exit_duty(SlotNumber::new(4)), + ]; + + (expired_duties, non_expired_duties, voluntary_exits) + } + + /// Helper function to add duties to the deadliner and send results to a + /// channel. + async fn add_duties( + duties: Vec, + deadliner: Arc, + result_tx: tokio::sync::mpsc::Sender, + ) { + for duty in duties { + let added = deadliner.add(duty).await; + let _ = result_tx.send(added).await; + } + } + + #[tokio::test] + async fn test_deadliner() { + let (expired_duties, non_expired_duties, voluntary_exits) = setup_data(); + + let start_time = DateTime::from_timestamp(1000, 0).unwrap(); + let clock = Arc::new(TestClock::new(start_time)); + + // Create a deadline function provider + let expired_set: std::collections::HashSet<_> = expired_duties.iter().cloned().collect(); + let deadline_func: DeadlineFunc = { + Arc::new(move |duty: Duty| { + if duty.duty_type == DutyType::Exit { + // Voluntary exits expire after 1 hour + let deadline = start_time + .checked_add_signed(chrono::Duration::try_hours(1).unwrap()) + .ok_or(DeadlineError::DateTimeCalculation)?; + return Ok(Some(deadline)); + } + + if expired_set.contains(&duty) { + // Expired duties have deadline 1 hour in the past + let deadline = start_time + .checked_sub_signed(chrono::Duration::try_hours(1).unwrap()) + .ok_or(DeadlineError::DateTimeCalculation)?; + return Ok(Some(deadline)); + } + + // Non-expired duties expire after duty.slot seconds from start + let deadline = start_time + .checked_add_signed( + chrono::Duration::try_seconds(i64::try_from(duty.slot.inner()).unwrap()) + .unwrap(), + ) + .ok_or(DeadlineError::DateTimeCalculation)?; + Ok(Some(deadline)) + }) + }; + + let cancel_token = CancellationToken::new(); + let deadliner = new_deadliner_for_test( + cancel_token.clone(), + "test", + deadline_func, + Arc::clone(&clock), + ); + + // Get the output receiver + let mut output_rx = deadliner.c().expect("should get receiver"); + + // Separate channels for expired and non-expired results + let (expired_tx, mut expired_rx) = tokio::sync::mpsc::channel(100); + let (non_expired_tx, mut non_expired_rx) = tokio::sync::mpsc::channel(100); + + // Add all duties + let expired_len = expired_duties.len(); + let non_expired_len = non_expired_duties.len(); + let voluntary_exits_len = voluntary_exits.len(); + + let handler_expired = tokio::spawn(add_duties( + expired_duties, + Arc::clone(&deadliner), + expired_tx, + )); + let handler_non_expired = tokio::spawn(add_duties( + non_expired_duties.clone(), + Arc::clone(&deadliner), + non_expired_tx.clone(), + )); + let handler_voluntary_exits = tokio::spawn(add_duties( + voluntary_exits, + Arc::clone(&deadliner), + non_expired_tx, + )); + + // Wait for all handlers to complete + let (result_expired, result_non_expired, result_voluntary_exits) = tokio::join!( + handler_expired, + handler_non_expired, + handler_voluntary_exits + ); + result_expired.unwrap(); + result_non_expired.unwrap(); + result_voluntary_exits.unwrap(); + + for _ in 0..expired_len { + let result = expired_rx.recv().await.expect("should receive result"); + assert!(!result, "expired duties should return false"); + } + + for _ in 0..(non_expired_len.checked_add(voluntary_exits_len).unwrap()) { + let result = non_expired_rx.recv().await.expect("should receive result"); + assert!(result, "non-expired duties should return true"); + } + + // Find max slot from non-expired duties + let max_slot = non_expired_duties + .iter() + .map(|d| d.slot.inner()) + .max() + .unwrap(); + + // Advance clock to trigger deadline of all non-expired duties + clock.advance(std::time::Duration::from_secs(max_slot)); + + // Give the deadliner task time to wake up and process + // We need to yield multiple times to ensure the background task runs + for _ in 0..10 { + tokio::task::yield_now().await; + } + + // Collect expired duties from output channel + let mut actual_duties = Vec::new(); + for _ in 0..non_expired_len { + let duty = tokio::time::timeout(std::time::Duration::from_secs(1), output_rx.recv()) + .await + .expect("should receive within timeout") + .expect("should receive duty"); + actual_duties.push(duty); + } + + // Sort both for comparison + actual_duties.sort_by_key(|d| d.slot.inner()); + let mut expected_duties = non_expired_duties; + expected_duties.sort_by_key(|d| d.slot.inner()); + + assert_eq!(expected_duties, actual_duties); + + cancel_token.cancel(); + } + + #[test_case(DutyType::Exit ; "exit")] + #[test_case(DutyType::BuilderRegistration ; "builder_registration")] + #[tokio::test] + async fn test_never_expire_duties(duty_type: DutyType) { + // Create a simple mock client that returns fixed values + let mock_client = create_mock_client(); + + let deadline_func = new_duty_deadline_func(&mock_client) + .await + .expect("should create deadline func"); + + let duty = Duty::new(SlotNumber::new(100), duty_type); + let result = deadline_func(duty).expect("should compute deadline"); + + assert_eq!(result, None, "duty should never expire"); + } + + // todo: uses hardcode beacon client for testing, should be refactored to use a + // real beacon client (testutils/beaconmock) + #[test_case(DutyType::Proposer ; "proposer")] + #[test_case(DutyType::Attester ; "attester")] + #[test_case(DutyType::Aggregator ; "aggregator")] + #[test_case(DutyType::PrepareAggregator ; "prepare_aggregator")] + #[test_case(DutyType::SyncMessage ; "sync_message")] + #[test_case(DutyType::SyncContribution ; "sync_contribution")] + #[test_case(DutyType::Randao ; "randao")] + #[test_case(DutyType::InfoSync ; "info_sync")] + #[test_case(DutyType::PrepareSyncContribution ; "prepare_sync_contribution")] + #[tokio::test] + async fn test_duty_deadline_durations(duty_type: DutyType) { + let mock_client = create_mock_client(); + + let genesis_time = mock_client.fetch_genesis_time().await.unwrap(); + let (slot_duration, _) = mock_client.fetch_slots_config().await.unwrap(); + + let margin = slot_duration + .checked_div(12) + .expect("margin calculation should not fail"); + + let time_since_genesis = Utc::now().signed_duration_since(genesis_time); + let slot_duration_chrono = to_chrono_duration(slot_duration).unwrap(); + let current_slot = u64::try_from( + time_since_genesis + .num_seconds() + .checked_div(slot_duration_chrono.num_seconds()) + .expect("slot duration should not be zero"), + ) + .expect("current slot should be positive"); + + let slot_start = { + let offset_secs = current_slot + .checked_mul(slot_duration.as_secs()) + .expect("slot offset should not overflow"); + let offset = chrono::Duration::try_seconds( + i64::try_from(offset_secs).expect("offset should fit in i64"), + ) + .expect("offset should be valid duration"); + genesis_time + .checked_add_signed(offset) + .expect("slot start should not overflow") + }; + + let deadline_func = new_duty_deadline_func(&mock_client) + .await + .expect("should create deadline func"); + + // Calculate expected duration based on duty type (matches Go test cases) + let expected_duration = match duty_type { + DutyType::Proposer | DutyType::Randao => { + // slotDuration/3 + margin + slot_duration + .checked_div(3) + .and_then(|d| d.checked_add(margin)) + .expect("duration calculation should not fail") + } + DutyType::Attester | DutyType::Aggregator | DutyType::PrepareAggregator => { + // 2*slotDuration + margin + slot_duration + .checked_mul(2) + .and_then(|d| d.checked_add(margin)) + .expect("duration calculation should not fail") + } + DutyType::SyncMessage => { + // 2*slotDuration/3 + margin + slot_duration + .checked_mul(2) + .and_then(|d| d.checked_div(3)) + .and_then(|d| d.checked_add(margin)) + .expect("duration calculation should not fail") + } + DutyType::SyncContribution | DutyType::InfoSync | DutyType::PrepareSyncContribution => { + // slotDuration + margin + slot_duration + .checked_add(margin) + .expect("duration calculation should not fail") + } + _ => panic!("unexpected duty type: {:?}", duty_type), + }; + + let duty = Duty::new(SlotNumber::new(current_slot), duty_type.clone()); + + // Matches Go: now := now.Add(tt.expectedDuration - time.Millisecond) + // This sets "now" to 1ms before the expected deadline + let now_before_deadline = slot_start + .checked_add_signed(to_chrono_duration(expected_duration).unwrap()) + .and_then(|t| t.checked_sub_signed(chrono::Duration::try_milliseconds(1).unwrap())) + .expect("time calculation should not fail"); + + // Call deadline function (matches Go: end, ok := deadlineFunc(tt.duty)) + let deadline_opt = deadline_func(duty.clone()).expect("should compute deadline"); + + assert!( + deadline_opt.is_some(), + "duty {:?} should have a deadline", + duty_type + ); + + let deadline = deadline_opt.unwrap(); + + // Matches Go: require.True(t, now.Before(end), "wrong duty deadline") + assert!( + now_before_deadline < deadline, + "duty {:?}: now ({}) should be before deadline ({})", + duty_type, + now_before_deadline, + deadline + ); + + // Matches Go: require.Equal(t, time.Millisecond, end.Sub(now)) + let time_until_deadline = deadline.signed_duration_since(now_before_deadline); + assert_eq!( + time_until_deadline, + chrono::Duration::try_milliseconds(1).unwrap(), + "duty {:?}: deadline should be exactly 1ms after now (actual: {}ms)", + duty_type, + time_until_deadline.num_milliseconds() + ); + } + + /// Creates a mock EthBeaconNodeApiClient for testing. + fn create_mock_client() -> MockBeaconClient { + MockBeaconClient { + genesis_time: DateTime::from_timestamp(1646092800, 0).unwrap(), /* 2022-03-01 + * 00:00:00 UTC */ + slot_duration: std::time::Duration::from_secs(12), + slots_per_epoch: 16, + } + } + + /// Mock beacon client for testing. + struct MockBeaconClient { + genesis_time: DateTime, + slot_duration: std::time::Duration, + slots_per_epoch: u64, + } + + #[async_trait] + impl BeaconClientForDeadline for MockBeaconClient { + async fn fetch_genesis_time(&self) -> Result> { + Ok(self.genesis_time) + } + + async fn fetch_slots_config(&self) -> Result<(std::time::Duration, u64)> { + Ok((self.slot_duration, self.slots_per_epoch)) + } + } +} From 368177c23fb940c9507582f7aa8d076716a5d7f0 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:22:00 +0100 Subject: [PATCH 03/11] fix: remove comments --- crates/core/src/deadline.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/core/src/deadline.rs b/crates/core/src/deadline.rs index 567113ea..47cd7fd4 100644 --- a/crates/core/src/deadline.rs +++ b/crates/core/src/deadline.rs @@ -767,7 +767,6 @@ mod tests { #[test_case(DutyType::BuilderRegistration ; "builder_registration")] #[tokio::test] async fn test_never_expire_duties(duty_type: DutyType) { - // Create a simple mock client that returns fixed values let mock_client = create_mock_client(); let deadline_func = new_duty_deadline_func(&mock_client) @@ -829,7 +828,6 @@ mod tests { .await .expect("should create deadline func"); - // Calculate expected duration based on duty type (matches Go test cases) let expected_duration = match duty_type { DutyType::Proposer | DutyType::Randao => { // slotDuration/3 + margin @@ -864,14 +862,11 @@ mod tests { let duty = Duty::new(SlotNumber::new(current_slot), duty_type.clone()); - // Matches Go: now := now.Add(tt.expectedDuration - time.Millisecond) - // This sets "now" to 1ms before the expected deadline let now_before_deadline = slot_start .checked_add_signed(to_chrono_duration(expected_duration).unwrap()) .and_then(|t| t.checked_sub_signed(chrono::Duration::try_milliseconds(1).unwrap())) .expect("time calculation should not fail"); - // Call deadline function (matches Go: end, ok := deadlineFunc(tt.duty)) let deadline_opt = deadline_func(duty.clone()).expect("should compute deadline"); assert!( @@ -882,7 +877,6 @@ mod tests { let deadline = deadline_opt.unwrap(); - // Matches Go: require.True(t, now.Before(end), "wrong duty deadline") assert!( now_before_deadline < deadline, "duty {:?}: now ({}) should be before deadline ({})", @@ -891,7 +885,6 @@ mod tests { deadline ); - // Matches Go: require.Equal(t, time.Millisecond, end.Sub(now)) let time_until_deadline = deadline.signed_duration_since(now_before_deadline); assert_eq!( time_until_deadline, From d71c8850970d3f44a9f7b18a9b13379bbf313ff6 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:17:24 +0100 Subject: [PATCH 04/11] refactor: remove old app/deadline --- crates/app/src/deadline/mod.rs | 53 ---------------------------------- crates/app/src/lib.rs | 3 -- 2 files changed, 56 deletions(-) delete mode 100644 crates/app/src/deadline/mod.rs diff --git a/crates/app/src/deadline/mod.rs b/crates/app/src/deadline/mod.rs deleted file mode 100644 index a8c39809..00000000 --- a/crates/app/src/deadline/mod.rs +++ /dev/null @@ -1,53 +0,0 @@ -use pluto_core::types::{Duty, DutyType}; -use pluto_eth2api::{EthBeaconNodeApiClient, EthBeaconNodeApiClientError}; - -/// Defines the fraction of the slot duration to use as a margin. -/// This is to consider network delays and other factors that may affect the -/// timing. -pub const MARGIN_FACTOR: u32 = 12; - -/// A function that returns the deadline for a duty. -pub type DeadlineFunc = Box Option> + Send + Sync>; - -/// Error type for deadline-related operations. -#[derive(Debug, thiserror::Error)] -pub enum DeadlineError { - /// Beacon client API error. - #[error("Beacon client error: {0}")] - BeaconClientError(#[from] EthBeaconNodeApiClientError), -} - -type Result = std::result::Result; - -/// Create a function that provides duty deadline or [`None`] if the duty never -/// deadlines. -pub async fn new_duty_deadline_func(eth2_cl: &EthBeaconNodeApiClient) -> Result { - let genesis_time = eth2_cl.fetch_genesis_time().await?; - let (slot_duration, _) = eth2_cl.fetch_slots_config().await?; - - #[allow( - clippy::arithmetic_side_effects, - reason = "Matches original implementation" - )] - Ok(Box::new(move |duty: Duty| match duty.duty_type { - DutyType::Exit | DutyType::BuilderRegistration => None, - _ => { - #[allow( - clippy::cast_possible_truncation, - reason = "TODO: unsupported operation in u64" - )] - let start = genesis_time + (slot_duration * (u64::from(duty.slot)) as u32); - let margin = slot_duration / MARGIN_FACTOR; - - let duration = match duty.duty_type { - DutyType::Proposer | DutyType::Randao => slot_duration / 3, - DutyType::SyncMessage => 2 * slot_duration / 3, - DutyType::Attester | DutyType::Aggregator | DutyType::PrepareAggregator => { - 2 * slot_duration - } - _ => slot_duration, - }; - Some(start + duration + margin) - } - })) -} diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index 3252fe67..5602c21c 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -13,9 +13,6 @@ pub mod log; /// until the deadline has elapsed. pub mod retry; -/// Deadline -pub mod deadline; - /// Featureset defines a set of global features and their rollout status. pub mod featureset; From a3561f6df56cd3ed7b60c9f8b07d8147f7117fae Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:03:04 +0100 Subject: [PATCH 05/11] fix: review comments --- Cargo.lock | 1 + crates/core/Cargo.toml | 2 + crates/core/src/deadline.rs | 452 +++++++++++------------------------- 3 files changed, 143 insertions(+), 312 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 261b8039..21334b4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5538,6 +5538,7 @@ dependencies = [ "tokio-util", "tracing", "tree_hash", + "wiremock", ] [[package]] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index cdb9c9aa..2e6f985d 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -38,6 +38,8 @@ prost-types.workspace = true hex.workspace = true chrono.workspace = true test-case.workspace = true +tokio = { workspace = true, features = ["test-util"] } +wiremock.workspace = true [build-dependencies] pluto-build-proto.workspace = true diff --git a/crates/core/src/deadline.rs b/crates/core/src/deadline.rs index 47cd7fd4..ca43caaa 100644 --- a/crates/core/src/deadline.rs +++ b/crates/core/src/deadline.rs @@ -39,9 +39,8 @@ //! # } //! ``` use crate::types::{Duty, DutyType, SlotNumber}; -use async_trait::async_trait; use chrono::{DateTime, Utc}; -use futures::future::{BoxFuture, FutureExt}; +use futures::future::BoxFuture; use pluto_eth2api::{EthBeaconNodeApiClient, EthBeaconNodeApiClientError}; use std::{ collections::HashSet, @@ -91,12 +90,6 @@ fn to_chrono_duration(duration: std::time::Duration) -> Result chrono::Duration::from_std(duration).map_err(|_| DeadlineError::DurationConversion) } -/// Converts seconds (u64) to `chrono::Duration`. -fn secs_to_chrono(secs: u64) -> Result { - let secs_i64 = i64::try_from(secs).map_err(|_| DeadlineError::ArithmeticOverflow)?; - chrono::Duration::try_seconds(secs_i64).ok_or(DeadlineError::DurationConversion) -} - /// Deadliner provides duty deadline functionality. /// /// The `c()` method returns a channel for receiving expired duties. @@ -122,35 +115,6 @@ pub trait Deadliner: Send + Sync { fn c(&self) -> Option>; } -/// Trait for beacon clients that can provide genesis time and slot -/// configuration. -/// -/// This trait abstracts the necessary beacon node API calls for deadline -/// calculation. -#[async_trait] -pub trait BeaconClientForDeadline { - /// Fetches the genesis time from the beacon node. - async fn fetch_genesis_time(&self) -> Result>; - - /// Fetches the slot duration and slots per epoch from the beacon node. - async fn fetch_slots_config(&self) -> Result<(std::time::Duration, u64)>; -} - -#[async_trait] -impl BeaconClientForDeadline for EthBeaconNodeApiClient { - async fn fetch_genesis_time(&self) -> Result> { - self.fetch_genesis_time() - .await - .map_err(DeadlineError::FetchGenesisTime) - } - - async fn fetch_slots_config(&self) -> Result<(std::time::Duration, u64)> { - self.fetch_slots_config() - .await - .map_err(DeadlineError::FetchGenesisTime) - } -} - /// Creates a deadline function from the Ethereum 2.0 beacon node configuration. /// /// Fetches genesis time and slot duration from the beacon node and returns @@ -159,11 +123,17 @@ impl BeaconClientForDeadline for EthBeaconNodeApiClient { /// # Errors /// /// Returns an error if fetching genesis time or slots config fails. -pub async fn new_duty_deadline_func( - client: &C, +pub async fn new_duty_deadline_func( + client: &EthBeaconNodeApiClient, ) -> Result { - let genesis_time = client.fetch_genesis_time().await?; - let (slot_duration, _slots_per_epoch) = client.fetch_slots_config().await?; + let genesis_time = client + .fetch_genesis_time() + .await + .map_err(DeadlineError::FetchGenesisTime)?; + let (slot_duration, _slots_per_epoch) = client + .fetch_slots_config() + .await + .map_err(DeadlineError::FetchGenesisTime)?; // Convert std::time::Duration to chrono::Duration for slot_duration let slot_duration = to_chrono_duration(slot_duration)?; @@ -187,7 +157,9 @@ pub async fn new_duty_deadline_func( .map_err(|_| DeadlineError::ArithmeticOverflow)?, ) .ok_or(DeadlineError::ArithmeticOverflow)?; - let slot_offset = secs_to_chrono(slot_secs)?; + let secs_i64 = i64::try_from(slot_secs).map_err(|_| DeadlineError::ArithmeticOverflow)?; + let slot_offset = + chrono::Duration::try_seconds(secs_i64).ok_or(DeadlineError::DurationConversion)?; let start: DateTime = genesis_time .checked_add_signed(slot_offset) @@ -246,16 +218,11 @@ fn get_curr_duty(duties: &HashSet, deadline_func: &DeadlineFunc) -> (Duty, // Use far-future sentinel date (9999-01-01) matching Go implementation // This timestamp is a known constant and will never fail - let mut curr_deadline = - DateTime::from_timestamp(253402300799, 0).unwrap_or(DateTime::::MAX_UTC); + let mut curr_deadline = DateTime::::MAX_UTC; for duty in duties.iter() { - let Ok(deadline_opt) = deadline_func(duty.clone()) else { - continue; - }; - // Ignore duties that never expire - let Some(duty_deadline) = deadline_opt else { + let Ok(Some(duty_deadline)) = deadline_func(duty.clone()) else { continue; }; @@ -279,7 +246,7 @@ struct DeadlineInput { struct DeadlinerImpl { cancel_token: CancellationToken, input_tx: tokio::sync::mpsc::UnboundedSender, - output_rx: Arc>>>, + output_rx: Mutex>>, } impl Deadliner for DeadlinerImpl { @@ -311,28 +278,6 @@ impl Deadliner for DeadlinerImpl { } } -/// Clock trait for abstracting time operations. -trait Clock: Send + Sync { - /// Returns the current time. - fn now(&self) -> DateTime; - - /// Creates a sleep future that completes after the given duration. - fn sleep(&self, duration: std::time::Duration) -> BoxFuture<'static, ()>; -} - -/// Real clock implementation using tokio::time. -struct RealClock; - -impl Clock for RealClock { - fn now(&self) -> DateTime { - Utc::now() - } - - fn sleep(&self, duration: std::time::Duration) -> BoxFuture<'static, ()> { - tokio::time::sleep(duration).boxed() - } -} - impl DeadlinerImpl { /// Background task that manages duty deadlines. /// @@ -342,7 +287,6 @@ impl DeadlinerImpl { cancel_token: CancellationToken, label: String, deadline_func: DeadlineFunc, - clock: Arc, mut input_rx: tokio::sync::mpsc::UnboundedReceiver, output_tx: tokio::sync::mpsc::Sender, ) { @@ -350,12 +294,13 @@ impl DeadlinerImpl { let (mut curr_duty, mut curr_deadline) = get_curr_duty(&duties, &deadline_func); // Create initial timer - let now = clock.now(); + let now = Utc::now(); let initial_duration = curr_deadline .signed_duration_since(now) .to_std() .unwrap_or(std::time::Duration::ZERO); - let mut timer = clock.sleep(initial_duration); + let sleep = tokio::time::sleep(initial_duration); + tokio::pin!(sleep); loop { tokio::select! { @@ -378,7 +323,7 @@ impl DeadlinerImpl { continue; }; - let now = clock.now(); + let now = Utc::now(); let expired = deadline < now; let _ = input.response_tx.send(!expired); @@ -398,14 +343,14 @@ impl DeadlinerImpl { curr_deadline = new_deadline; let duration = curr_deadline - .signed_duration_since(clock.now()) + .signed_duration_since(Utc::now()) .to_std() .unwrap_or(std::time::Duration::ZERO); - timer = clock.sleep(duration); + sleep.set(tokio::time::sleep(duration)); } } - _ = &mut timer => { + _ = &mut sleep => { // Deadline expired - send duty to output channel match output_tx.try_send(curr_duty.clone()) { Ok(()) => {} @@ -430,46 +375,14 @@ impl DeadlinerImpl { curr_deadline = new_deadline; let duration = curr_deadline - .signed_duration_since(clock.now()) + .signed_duration_since(Utc::now()) .to_std() .unwrap_or(std::time::Duration::ZERO); - timer = clock.sleep(duration); + sleep.set(tokio::time::sleep(duration)); } } } } - - /// Internal constructor for creating a deadliner with a specific clock. - fn new_internal( - cancel_token: CancellationToken, - label: impl Into, - deadline_func: DeadlineFunc, - clock: Arc, - ) -> Arc { - const OUTPUT_BUFFER: usize = 10; - - let label = label.into(); - let (input_tx, input_rx) = tokio::sync::mpsc::unbounded_channel(); - let (output_tx, output_rx) = tokio::sync::mpsc::channel(OUTPUT_BUFFER); - - let impl_instance: Arc = Arc::new(DeadlinerImpl { - cancel_token: cancel_token.clone(), - input_tx, - output_rx: Arc::new(Mutex::new(Some(output_rx))), - }); - - // Spawn background task - tokio::spawn(Self::run_task( - cancel_token, - label, - deadline_func, - clock, - input_rx, - output_tx, - )); - - impl_instance - } } /// Creates a new Deadliner instance. @@ -492,111 +405,83 @@ pub fn new_deadliner( label: impl Into, deadline_func: DeadlineFunc, ) -> Arc { - DeadlinerImpl::new_internal(cancel_token, label, deadline_func, Arc::new(RealClock)) -} - -/// Creates a new Deadliner instance for testing with a fake clock. -/// -/// This constructor is intended for use in tests where you need to control -/// time progression. -/// -/// # Arguments -/// -/// * `cancel_token` - Token to cancel the background task -/// * `label` - Label for logging purposes -/// * `deadline_func` - Function that calculates deadlines for duties -/// * `clock` - Test clock for controlling time in tests -/// -/// # Returns -/// -/// An Arc-wrapped Deadliner trait object -#[cfg(test)] -fn new_deadliner_for_test( - cancel_token: CancellationToken, - label: impl Into, - deadline_func: DeadlineFunc, - clock: Arc, -) -> Arc { - DeadlinerImpl::new_internal(cancel_token, label, deadline_func, clock) + const OUTPUT_BUFFER: usize = 10; + + let label = label.into(); + let (input_tx, input_rx) = tokio::sync::mpsc::unbounded_channel(); + let (output_tx, output_rx) = tokio::sync::mpsc::channel(OUTPUT_BUFFER); + + let impl_instance: Arc = Arc::new(DeadlinerImpl { + cancel_token: cancel_token.clone(), + input_tx, + output_rx: Mutex::new(Some(output_rx)), + }); + + // Spawn background task + tokio::spawn(DeadlinerImpl::run_task( + cancel_token, + label, + deadline_func, + input_rx, + output_tx, + )); + + impl_instance } -/// Fake clock implementation for testing. #[cfg(test)] -type WakerList = Vec<(DateTime, std::task::Waker)>; - -#[cfg(test)] -struct TestClock { - start: std::sync::Arc>>, - wakers: std::sync::Arc>, -} +mod tests { + use std::time::Duration; -#[cfg(test)] -impl TestClock { - fn new(start: DateTime) -> Self { - Self { - start: std::sync::Arc::new(std::sync::Mutex::new(start)), - wakers: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), - } - } + use super::*; + use crate::types::SlotNumber; + use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, + }; - fn advance(&self, duration: std::time::Duration) { - let new_time = { - let mut start = self.start.lock().unwrap(); - let chrono_duration = chrono::Duration::from_std(duration).unwrap(); - *start = start.checked_add_signed(chrono_duration).unwrap(); - *start - }; + /// Creates a mock beacon node API server and returns the client. + async fn create_mock_beacon_client( + genesis_time: DateTime, + slot_duration_secs: u64, + slots_per_epoch: u64, + ) -> (MockServer, EthBeaconNodeApiClient) { + let mock_server = MockServer::start().await; + + // Mock /eth/v1/beacon/genesis + let genesis_response = serde_json::json!({ + "data": { + "genesis_time": genesis_time.timestamp().to_string(), + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "genesis_fork_version": "0x00000000" + } + }); - // Wake all timers that have expired - let mut wakers = self.wakers.lock().unwrap(); - let (expired, pending): (Vec<_>, Vec<_>) = wakers - .drain(..) - .partition(|(deadline, _)| *deadline <= new_time); - *wakers = pending; + Mock::given(method("GET")) + .and(path("/eth/v1/beacon/genesis")) + .respond_with(ResponseTemplate::new(200).set_body_json(genesis_response)) + .mount(&mock_server) + .await; + + // Mock /eth/v1/config/spec + let spec_response = serde_json::json!({ + "data": { + "SECONDS_PER_SLOT": slot_duration_secs.to_string(), + "SLOTS_PER_EPOCH": slots_per_epoch.to_string() + } + }); - // Wake expired futures - for (_, waker) in expired { - waker.wake(); - } - } -} + Mock::given(method("GET")) + .and(path("/eth/v1/config/spec")) + .respond_with(ResponseTemplate::new(200).set_body_json(spec_response)) + .mount(&mock_server) + .await; -#[cfg(test)] -impl Clock for TestClock { - fn now(&self) -> DateTime { - *self.start.lock().unwrap() - } + let client = EthBeaconNodeApiClient::with_base_url(mock_server.uri()) + .expect("Failed to create client"); - fn sleep(&self, duration: std::time::Duration) -> BoxFuture<'static, ()> { - let deadline = self - .now() - .checked_add_signed(chrono::Duration::from_std(duration).unwrap()) - .unwrap(); - let wakers = Arc::clone(&self.wakers); - let start = Arc::clone(&self.start); - - Box::pin(std::future::poll_fn(move |cx| { - let now = *start.lock().unwrap(); - if now >= deadline { - std::task::Poll::Ready(()) - } else { - // Register waker - let mut wakers = wakers.lock().unwrap(); - // Check if this waker is already registered for this deadline - if !wakers.iter().any(|(d, _)| *d == deadline) { - wakers.push((deadline, cx.waker().clone())); - } - std::task::Poll::Pending - } - })) + (mock_server, client) } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::types::SlotNumber; - use test_case::test_case; /// Helper function to create expired duties, non-expired duties, and /// voluntary exits. @@ -637,15 +522,15 @@ mod tests { async fn test_deadliner() { let (expired_duties, non_expired_duties, voluntary_exits) = setup_data(); - let start_time = DateTime::from_timestamp(1000, 0).unwrap(); - let clock = Arc::new(TestClock::new(start_time)); + // Use real time with short durations (milliseconds instead of hours/seconds) + let start_time = Utc::now(); // Create a deadline function provider let expired_set: std::collections::HashSet<_> = expired_duties.iter().cloned().collect(); let deadline_func: DeadlineFunc = { Arc::new(move |duty: Duty| { if duty.duty_type == DutyType::Exit { - // Voluntary exits expire after 1 hour + // Voluntary exits expire after 1 hour (far in the future) let deadline = start_time .checked_add_signed(chrono::Duration::try_hours(1).unwrap()) .ok_or(DeadlineError::DateTimeCalculation)?; @@ -660,11 +545,17 @@ mod tests { return Ok(Some(deadline)); } - // Non-expired duties expire after duty.slot seconds from start + // Non-expired duties expire after duty.slot * 10 milliseconds from now + // This gives us short, deterministic deadlines for testing let deadline = start_time .checked_add_signed( - chrono::Duration::try_seconds(i64::try_from(duty.slot.inner()).unwrap()) - .unwrap(), + chrono::Duration::try_milliseconds( + i64::try_from(duty.slot.inner()) + .unwrap() + .checked_mul(10) + .unwrap(), + ) + .unwrap(), ) .ok_or(DeadlineError::DateTimeCalculation)?; Ok(Some(deadline)) @@ -672,12 +563,7 @@ mod tests { }; let cancel_token = CancellationToken::new(); - let deadliner = new_deadliner_for_test( - cancel_token.clone(), - "test", - deadline_func, - Arc::clone(&clock), - ); + let deadliner = new_deadliner(cancel_token.clone(), "test", deadline_func); // Get the output receiver let mut output_rx = deadliner.c().expect("should get receiver"); @@ -727,23 +613,8 @@ mod tests { assert!(result, "non-expired duties should return true"); } - // Find max slot from non-expired duties - let max_slot = non_expired_duties - .iter() - .map(|d| d.slot.inner()) - .max() - .unwrap(); - - // Advance clock to trigger deadline of all non-expired duties - clock.advance(std::time::Duration::from_secs(max_slot)); - - // Give the deadliner task time to wake up and process - // We need to yield multiple times to ensure the background task runs - for _ in 0..10 { - tokio::task::yield_now().await; - } - // Collect expired duties from output channel + // Use a generous timeout since we're using real time let mut actual_duties = Vec::new(); for _ in 0..non_expired_len { let duty = tokio::time::timeout(std::time::Duration::from_secs(1), output_rx.recv()) @@ -763,13 +634,18 @@ mod tests { cancel_token.cancel(); } - #[test_case(DutyType::Exit ; "exit")] - #[test_case(DutyType::BuilderRegistration ; "builder_registration")] + #[test_case::test_case(DutyType::Exit ; "exit")] + #[test_case::test_case(DutyType::BuilderRegistration ; "builder_registration")] #[tokio::test] async fn test_never_expire_duties(duty_type: DutyType) { - let mock_client = create_mock_client(); + let genesis_time = DateTime::from_timestamp(1606824023, 0).unwrap(); + let slot_duration_secs = 12; + let slots_per_epoch = 32; + + let (_mock_server, client) = + create_mock_beacon_client(genesis_time, slot_duration_secs, slots_per_epoch).await; - let deadline_func = new_duty_deadline_func(&mock_client) + let deadline_func = new_duty_deadline_func(&client) .await .expect("should create deadline func"); @@ -779,37 +655,31 @@ mod tests { assert_eq!(result, None, "duty should never expire"); } - // todo: uses hardcode beacon client for testing, should be refactored to use a - // real beacon client (testutils/beaconmock) - #[test_case(DutyType::Proposer ; "proposer")] - #[test_case(DutyType::Attester ; "attester")] - #[test_case(DutyType::Aggregator ; "aggregator")] - #[test_case(DutyType::PrepareAggregator ; "prepare_aggregator")] - #[test_case(DutyType::SyncMessage ; "sync_message")] - #[test_case(DutyType::SyncContribution ; "sync_contribution")] - #[test_case(DutyType::Randao ; "randao")] - #[test_case(DutyType::InfoSync ; "info_sync")] - #[test_case(DutyType::PrepareSyncContribution ; "prepare_sync_contribution")] + #[test_case::test_case(DutyType::Proposer ; "proposer")] + #[test_case::test_case(DutyType::Attester ; "attester")] + #[test_case::test_case(DutyType::Aggregator ; "aggregator")] + #[test_case::test_case(DutyType::PrepareAggregator ; "prepare_aggregator")] + #[test_case::test_case(DutyType::SyncMessage ; "sync_message")] + #[test_case::test_case(DutyType::SyncContribution ; "sync_contribution")] + #[test_case::test_case(DutyType::Randao ; "randao")] + #[test_case::test_case(DutyType::InfoSync ; "info_sync")] + #[test_case::test_case(DutyType::PrepareSyncContribution ; "prepare_sync_contribution")] #[tokio::test] async fn test_duty_deadline_durations(duty_type: DutyType) { - let mock_client = create_mock_client(); + let genesis_time = DateTime::from_timestamp(1606824023, 0).unwrap(); + let slot_duration_secs = 12; + let slots_per_epoch = 32; - let genesis_time = mock_client.fetch_genesis_time().await.unwrap(); - let (slot_duration, _) = mock_client.fetch_slots_config().await.unwrap(); + let (_mock_server, client) = + create_mock_beacon_client(genesis_time, slot_duration_secs, slots_per_epoch).await; + let slot_duration = Duration::from_secs(slot_duration_secs); let margin = slot_duration .checked_div(12) .expect("margin calculation should not fail"); - let time_since_genesis = Utc::now().signed_duration_since(genesis_time); - let slot_duration_chrono = to_chrono_duration(slot_duration).unwrap(); - let current_slot = u64::try_from( - time_since_genesis - .num_seconds() - .checked_div(slot_duration_chrono.num_seconds()) - .expect("slot duration should not be zero"), - ) - .expect("current slot should be positive"); + // Use a fixed slot for deterministic testing + let current_slot = 100u64; let slot_start = { let offset_secs = current_slot @@ -824,7 +694,7 @@ mod tests { .expect("slot start should not overflow") }; - let deadline_func = new_duty_deadline_func(&mock_client) + let deadline_func = new_duty_deadline_func(&client) .await .expect("should create deadline func"); @@ -857,69 +727,27 @@ mod tests { .checked_add(margin) .expect("duration calculation should not fail") } - _ => panic!("unexpected duty type: {:?}", duty_type), + _ => panic!("unexpected duty type: {duty_type:?}"), }; let duty = Duty::new(SlotNumber::new(current_slot), duty_type.clone()); - let now_before_deadline = slot_start + let expected_deadline = slot_start .checked_add_signed(to_chrono_duration(expected_duration).unwrap()) - .and_then(|t| t.checked_sub_signed(chrono::Duration::try_milliseconds(1).unwrap())) - .expect("time calculation should not fail"); + .expect("deadline calculation should not fail"); let deadline_opt = deadline_func(duty.clone()).expect("should compute deadline"); assert!( deadline_opt.is_some(), - "duty {:?} should have a deadline", - duty_type + "duty {duty_type:?} should have a deadline" ); let deadline = deadline_opt.unwrap(); - assert!( - now_before_deadline < deadline, - "duty {:?}: now ({}) should be before deadline ({})", - duty_type, - now_before_deadline, - deadline - ); - - let time_until_deadline = deadline.signed_duration_since(now_before_deadline); assert_eq!( - time_until_deadline, - chrono::Duration::try_milliseconds(1).unwrap(), - "duty {:?}: deadline should be exactly 1ms after now (actual: {}ms)", - duty_type, - time_until_deadline.num_milliseconds() + deadline, expected_deadline, + "duty {duty_type:?}: deadline mismatch" ); } - - /// Creates a mock EthBeaconNodeApiClient for testing. - fn create_mock_client() -> MockBeaconClient { - MockBeaconClient { - genesis_time: DateTime::from_timestamp(1646092800, 0).unwrap(), /* 2022-03-01 - * 00:00:00 UTC */ - slot_duration: std::time::Duration::from_secs(12), - slots_per_epoch: 16, - } - } - - /// Mock beacon client for testing. - struct MockBeaconClient { - genesis_time: DateTime, - slot_duration: std::time::Duration, - slots_per_epoch: u64, - } - - #[async_trait] - impl BeaconClientForDeadline for MockBeaconClient { - async fn fetch_genesis_time(&self) -> Result> { - Ok(self.genesis_time) - } - - async fn fetch_slots_config(&self) -> Result<(std::time::Duration, u64)> { - Ok((self.slot_duration, self.slots_per_epoch)) - } - } } From 1aecbee0dc9877996cc244df9cb2c4b610453da9 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:10:09 +0100 Subject: [PATCH 06/11] fix: linter --- Cargo.lock | 35 ++++++++++++++--------------------- crates/core/src/deadline.rs | 6 ++---- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21334b4f..38805fbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5495,7 +5495,6 @@ dependencies = [ "prost-build", "prost-types 0.14.3", "rand 0.8.5", - "rand_core 0.6.4", "reqwest 0.13.2", "serde", "serde_json", @@ -5512,33 +5511,21 @@ dependencies = [ name = "pluto-core" version = "1.7.1" dependencies = [ - "alloy", - "async-trait", - "base64 0.22.1", "built", "cancellation", "chrono", "crossbeam", - "futures", "hex", "libp2p", "pluto-build-proto", - "pluto-eth2api", - "pluto-eth2util", "prost 0.14.3", "prost-types 0.14.3", "rand 0.8.5", "regex", "serde", "serde_json", - "serde_with", - "test-case", "thiserror 2.0.18", "tokio", - "tokio-util", - "tracing", - "tree_hash", - "wiremock", ] [[package]] @@ -5547,6 +5534,7 @@ version = "1.7.1" dependencies = [ "blst", "hex", + "pluto-core", "pluto-eth2api", "rand 0.8.5", "rand_core 0.6.4", @@ -5559,9 +5547,22 @@ dependencies = [ name = "pluto-dkg" version = "1.7.1" dependencies = [ + "hex", "pluto-build-proto", + "pluto-cluster", + "pluto-crypto", + "pluto-eth1wrap", + "pluto-eth2util", "prost 0.14.3", "prost-types 0.14.3", + "rand 0.8.5", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", ] [[package]] @@ -5577,7 +5578,6 @@ dependencies = [ name = "pluto-eth2api" version = "1.7.1" dependencies = [ - "alloy", "anyhow", "bon", "chrono", @@ -5589,7 +5589,6 @@ dependencies = [ "serde", "serde_json", "serde_with", - "test-case", "testcontainers", "thiserror 2.0.18", "tokio", @@ -5622,7 +5621,6 @@ dependencies = [ "scrypt", "serde", "serde_json", - "serde_with", "sha2", "sha3", "tempfile", @@ -5657,22 +5655,18 @@ version = "1.7.1" dependencies = [ "anyhow", "axum", - "backon", "chrono", "clap", "either", "futures", - "hex", "k256", "libp2p", - "pluto-cluster", "pluto-core", "pluto-eth2util", "pluto-k1util", "pluto-testutil", "pluto-tracing", "rand 0.8.5", - "reqwest 0.13.2", "serde", "serde_json", "tempfile", @@ -5680,7 +5674,6 @@ dependencies = [ "tokio", "tokio-util", "tracing", - "url", "vise", "vise-exporter", ] diff --git a/crates/core/src/deadline.rs b/crates/core/src/deadline.rs index ca43caaa..d1a51569 100644 --- a/crates/core/src/deadline.rs +++ b/crates/core/src/deadline.rs @@ -123,9 +123,7 @@ pub trait Deadliner: Send + Sync { /// # Errors /// /// Returns an error if fetching genesis time or slots config fails. -pub async fn new_duty_deadline_func( - client: &EthBeaconNodeApiClient, -) -> Result { +pub async fn new_duty_deadline_func(client: &EthBeaconNodeApiClient) -> Result { let genesis_time = client .fetch_genesis_time() .await @@ -436,8 +434,8 @@ mod tests { use super::*; use crate::types::SlotNumber; use wiremock::{ - matchers::{method, path}, Mock, MockServer, ResponseTemplate, + matchers::{method, path}, }; /// Creates a mock beacon node API server and returns the client. From 1fe73d332a3067aacf43a21adbd3f826f9269e68 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:12:21 +0100 Subject: [PATCH 07/11] fix: update Cargo.lock --- Cargo.lock | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38805fbe..21334b4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5495,6 +5495,7 @@ dependencies = [ "prost-build", "prost-types 0.14.3", "rand 0.8.5", + "rand_core 0.6.4", "reqwest 0.13.2", "serde", "serde_json", @@ -5511,21 +5512,33 @@ dependencies = [ name = "pluto-core" version = "1.7.1" dependencies = [ + "alloy", + "async-trait", + "base64 0.22.1", "built", "cancellation", "chrono", "crossbeam", + "futures", "hex", "libp2p", "pluto-build-proto", + "pluto-eth2api", + "pluto-eth2util", "prost 0.14.3", "prost-types 0.14.3", "rand 0.8.5", "regex", "serde", "serde_json", + "serde_with", + "test-case", "thiserror 2.0.18", "tokio", + "tokio-util", + "tracing", + "tree_hash", + "wiremock", ] [[package]] @@ -5534,7 +5547,6 @@ version = "1.7.1" dependencies = [ "blst", "hex", - "pluto-core", "pluto-eth2api", "rand 0.8.5", "rand_core 0.6.4", @@ -5547,22 +5559,9 @@ dependencies = [ name = "pluto-dkg" version = "1.7.1" dependencies = [ - "hex", "pluto-build-proto", - "pluto-cluster", - "pluto-crypto", - "pluto-eth1wrap", - "pluto-eth2util", "prost 0.14.3", "prost-types 0.14.3", - "rand 0.8.5", - "serde", - "serde_json", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tracing", - "url", ] [[package]] @@ -5578,6 +5577,7 @@ dependencies = [ name = "pluto-eth2api" version = "1.7.1" dependencies = [ + "alloy", "anyhow", "bon", "chrono", @@ -5589,6 +5589,7 @@ dependencies = [ "serde", "serde_json", "serde_with", + "test-case", "testcontainers", "thiserror 2.0.18", "tokio", @@ -5621,6 +5622,7 @@ dependencies = [ "scrypt", "serde", "serde_json", + "serde_with", "sha2", "sha3", "tempfile", @@ -5655,18 +5657,22 @@ version = "1.7.1" dependencies = [ "anyhow", "axum", + "backon", "chrono", "clap", "either", "futures", + "hex", "k256", "libp2p", + "pluto-cluster", "pluto-core", "pluto-eth2util", "pluto-k1util", "pluto-testutil", "pluto-tracing", "rand 0.8.5", + "reqwest 0.13.2", "serde", "serde_json", "tempfile", @@ -5674,6 +5680,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "url", "vise", "vise-exporter", ] From ca1bc4ec769d3c6743aefdbd354dba13d223c9ab Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:24:30 +0100 Subject: [PATCH 08/11] fix: busy loop --- crates/core/src/deadline.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/core/src/deadline.rs b/crates/core/src/deadline.rs index d1a51569..26b5c4db 100644 --- a/crates/core/src/deadline.rs +++ b/crates/core/src/deadline.rs @@ -292,11 +292,15 @@ impl DeadlinerImpl { let (mut curr_duty, mut curr_deadline) = get_curr_duty(&duties, &deadline_func); // Create initial timer - let now = Utc::now(); - let initial_duration = curr_deadline - .signed_duration_since(now) - .to_std() - .unwrap_or(std::time::Duration::ZERO); + let now: DateTime = Utc::now(); + let initial_duration = if curr_deadline < now { + std::time::Duration::ZERO + } else { + curr_deadline + .signed_duration_since(now) + .to_std() + .unwrap_or(std::time::Duration::MAX) + }; let sleep = tokio::time::sleep(initial_duration); tokio::pin!(sleep); @@ -343,7 +347,7 @@ impl DeadlinerImpl { let duration = curr_deadline .signed_duration_since(Utc::now()) .to_std() - .unwrap_or(std::time::Duration::ZERO); + .unwrap_or(std::time::Duration::MAX); sleep.set(tokio::time::sleep(duration)); } } @@ -375,7 +379,7 @@ impl DeadlinerImpl { let duration = curr_deadline .signed_duration_since(Utc::now()) .to_std() - .unwrap_or(std::time::Duration::ZERO); + .unwrap_or(std::time::Duration::MAX); sleep.set(tokio::time::sleep(duration)); } } From dff48e43690fe30969ca46d10d87bd4e0e301f92 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:45:30 +0100 Subject: [PATCH 09/11] fix: claude review --- crates/core/src/deadline.rs | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/crates/core/src/deadline.rs b/crates/core/src/deadline.rs index 26b5c4db..8db3ab2e 100644 --- a/crates/core/src/deadline.rs +++ b/crates/core/src/deadline.rs @@ -61,9 +61,9 @@ pub type DeadlineFunc = Arc Result>> + Send /// Error types for deadline operations. #[derive(Debug, thiserror::Error)] pub enum DeadlineError { - /// Failed to fetch genesis time from beacon node. - #[error("Failed to fetch genesis time: {0}")] - FetchGenesisTime(#[from] EthBeaconNodeApiClientError), + /// Failed to fetch beacon node configuration. + #[error("Failed to fetch beacon node configuration: {0}")] + BeaconNodeConfigError(#[from] EthBeaconNodeApiClientError), /// Deadliner has been shut down. #[error("Deadliner has been shut down")] @@ -124,25 +124,19 @@ pub trait Deadliner: Send + Sync { /// /// Returns an error if fetching genesis time or slots config fails. pub async fn new_duty_deadline_func(client: &EthBeaconNodeApiClient) -> Result { - let genesis_time = client - .fetch_genesis_time() - .await - .map_err(DeadlineError::FetchGenesisTime)?; - let (slot_duration, _slots_per_epoch) = client - .fetch_slots_config() - .await - .map_err(DeadlineError::FetchGenesisTime)?; + let genesis_time = client.fetch_genesis_time().await?; + let (slot_duration, _slots_per_epoch) = client.fetch_slots_config().await?; // Convert std::time::Duration to chrono::Duration for slot_duration let slot_duration = to_chrono_duration(slot_duration)?; Ok(Arc::new(move |duty: Duty| { // Exit and BuilderRegistration duties never expire - match duty.duty_type { - DutyType::Exit | DutyType::BuilderRegistration => { - return Ok(None); - } - _ => {} + if matches!( + duty.duty_type, + DutyType::Exit | DutyType::BuilderRegistration + ) { + return Ok(None); } // Calculate slot start time @@ -243,7 +237,7 @@ struct DeadlineInput { /// Implementation of the Deadliner trait. struct DeadlinerImpl { cancel_token: CancellationToken, - input_tx: tokio::sync::mpsc::UnboundedSender, + input_tx: tokio::sync::mpsc::Sender, output_rx: Mutex>>, } @@ -259,7 +253,7 @@ impl Deadliner for DeadlinerImpl { let input = DeadlineInput { duty, response_tx }; // Send the duty to the background task - if self.input_tx.send(input).is_err() { + if self.input_tx.send(input).await.is_err() { return false; } @@ -285,7 +279,7 @@ impl DeadlinerImpl { cancel_token: CancellationToken, label: String, deadline_func: DeadlineFunc, - mut input_rx: tokio::sync::mpsc::UnboundedReceiver, + mut input_rx: tokio::sync::mpsc::Receiver, output_tx: tokio::sync::mpsc::Sender, ) { let mut duties: HashSet = HashSet::new(); @@ -407,10 +401,11 @@ pub fn new_deadliner( label: impl Into, deadline_func: DeadlineFunc, ) -> Arc { - const OUTPUT_BUFFER: usize = 10; + const OUTPUT_BUFFER: usize = 256; + const INPUT_BUFFER: usize = 256; let label = label.into(); - let (input_tx, input_rx) = tokio::sync::mpsc::unbounded_channel(); + let (input_tx, input_rx) = tokio::sync::mpsc::channel(INPUT_BUFFER); let (output_tx, output_rx) = tokio::sync::mpsc::channel(OUTPUT_BUFFER); let impl_instance: Arc = Arc::new(DeadlinerImpl { From fece6fce133d407ea3b4e6ebf3fa2126f816a279 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:03:04 +0100 Subject: [PATCH 10/11] fix: add far-future duration --- crates/core/src/deadline.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/core/src/deadline.rs b/crates/core/src/deadline.rs index 8db3ab2e..6fe367bd 100644 --- a/crates/core/src/deadline.rs +++ b/crates/core/src/deadline.rs @@ -51,6 +51,12 @@ use tokio_util::sync::CancellationToken; /// Fraction of slot duration to use as a margin for network delays. const MARGIN_FACTOR: i32 = 12; +/// A safe far-future duration (~10 years) for timeout calculations. +/// Using Duration::MAX can cause panics when computing Instant::now() + +/// duration, so we use a large but representable value instead. +const FAR_FUTURE_DURATION: std::time::Duration = + std::time::Duration::from_secs(3600 * 24 * 365 * 10); + /// Type alias for the deadline function. /// /// Takes a duty and returns an optional deadline. @@ -293,7 +299,7 @@ impl DeadlinerImpl { curr_deadline .signed_duration_since(now) .to_std() - .unwrap_or(std::time::Duration::MAX) + .unwrap_or(FAR_FUTURE_DURATION) }; let sleep = tokio::time::sleep(initial_duration); tokio::pin!(sleep); @@ -341,7 +347,7 @@ impl DeadlinerImpl { let duration = curr_deadline .signed_duration_since(Utc::now()) .to_std() - .unwrap_or(std::time::Duration::MAX); + .unwrap_or(FAR_FUTURE_DURATION); sleep.set(tokio::time::sleep(duration)); } } @@ -373,7 +379,7 @@ impl DeadlinerImpl { let duration = curr_deadline .signed_duration_since(Utc::now()) .to_std() - .unwrap_or(std::time::Duration::MAX); + .unwrap_or(FAR_FUTURE_DURATION); sleep.set(tokio::time::sleep(duration)); } } From 1719276bdfd14825bf693fadac0933a69f1af5e4 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:16:18 +0200 Subject: [PATCH 11/11] fix: review comments --- crates/core/src/deadline.rs | 141 +++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 65 deletions(-) diff --git a/crates/core/src/deadline.rs b/crates/core/src/deadline.rs index 6fe367bd..4e8193bb 100644 --- a/crates/core/src/deadline.rs +++ b/crates/core/src/deadline.rs @@ -39,8 +39,8 @@ //! # } //! ``` use crate::types::{Duty, DutyType, SlotNumber}; +use async_trait::async_trait; use chrono::{DateTime, Utc}; -use futures::future::BoxFuture; use pluto_eth2api::{EthBeaconNodeApiClient, EthBeaconNodeApiClientError}; use std::{ collections::HashSet, @@ -71,10 +71,6 @@ pub enum DeadlineError { #[error("Failed to fetch beacon node configuration: {0}")] BeaconNodeConfigError(#[from] EthBeaconNodeApiClientError), - /// Deadliner has been shut down. - #[error("Deadliner has been shut down")] - Shutdown, - /// Arithmetic overflow in deadline calculation. #[error("Arithmetic overflow in deadline calculation")] ArithmeticOverflow, @@ -102,6 +98,7 @@ fn to_chrono_duration(duration: std::time::Duration) -> Result /// It may only be called once and the returned channel should be used /// by a single task. Multiple instances are required for different /// components and use cases. +#[async_trait] pub trait Deadliner: Send + Sync { /// Adds a duty for deadline scheduling. /// @@ -112,7 +109,7 @@ pub trait Deadliner: Send + Sync { /// Returns `false` if: /// - The duty has already expired and cannot be scheduled /// - The duty never expires (e.g., Exit, BuilderRegistration) - fn add(&self, duty: Duty) -> BoxFuture<'_, bool>; + async fn add(&self, duty: Duty) -> bool; /// Returns the channel for receiving deadlined duties. /// @@ -219,15 +216,24 @@ fn get_curr_duty(duties: &HashSet, deadline_func: &DeadlineFunc) -> (Duty, let mut curr_deadline = DateTime::::MAX_UTC; for duty in duties.iter() { - // Ignore duties that never expire - let Ok(Some(duty_deadline)) = deadline_func(duty.clone()) else { - continue; - }; - - // Update if this duty has an earlier deadline - if duty_deadline < curr_deadline { - curr_duty = duty.clone(); - curr_deadline = duty_deadline; + match deadline_func(duty.clone()) { + Ok(Some(duty_deadline)) => { + // Update if this duty has an earlier deadline + if duty_deadline < curr_deadline { + curr_duty = duty.clone(); + curr_deadline = duty_deadline; + } + } + Err(err) => { + tracing::warn!( + duty = %duty, + error = %err, + "Failed to compute deadline for duty" + ); + } + Ok(None) => { + // Ignore duties that never expire + } } } @@ -247,25 +253,24 @@ struct DeadlinerImpl { output_rx: Mutex>>, } +#[async_trait] impl Deadliner for DeadlinerImpl { - fn add(&self, duty: Duty) -> BoxFuture<'_, bool> { - Box::pin(async move { - // Check if shut down - if self.cancel_token.is_cancelled() { - return false; - } + async fn add(&self, duty: Duty) -> bool { + // Check if shut down + if self.cancel_token.is_cancelled() { + return false; + } - let (response_tx, response_rx) = tokio::sync::oneshot::channel(); - let input = DeadlineInput { duty, response_tx }; + let (response_tx, response_rx) = tokio::sync::oneshot::channel(); + let input = DeadlineInput { duty, response_tx }; - // Send the duty to the background task - if self.input_tx.send(input).await.is_err() { - return false; - } + // Send the duty to the background task + if self.input_tx.send(input).await.is_err() { + return false; + } - // Wait for response - response_rx.await.unwrap_or(false) - }) + // Wait for response + response_rx.await.unwrap_or(false) } fn c(&self) -> Option> { @@ -314,41 +319,47 @@ impl DeadlinerImpl { Some(input) = input_rx.recv() => { let duty = input.duty; - let Ok(deadline_opt) = deadline_func(duty.clone()) else { - let _ = input.response_tx.send(false); - continue; - }; - - // Drop duties that never expire - let Some(deadline) = deadline_opt else { - let _ = input.response_tx.send(false); - continue; - }; - - let now = Utc::now(); - let expired = deadline < now; - - let _ = input.response_tx.send(!expired); - - // Ignore expired duties - if expired { - continue; - } - - // Add duty to the map (idempotent) - duties.insert(duty); - - // Update timer if this deadline is earlier - if deadline < curr_deadline { - let (new_duty, new_deadline) = get_curr_duty(&duties, &deadline_func); - curr_duty = new_duty; - curr_deadline = new_deadline; - - let duration = curr_deadline - .signed_duration_since(Utc::now()) - .to_std() - .unwrap_or(FAR_FUTURE_DURATION); - sleep.set(tokio::time::sleep(duration)); + match deadline_func(duty.clone()) { + Ok(Some(deadline)) => { + let now = Utc::now(); + let expired = deadline < now; + + let _ = input.response_tx.send(!expired); + + // Ignore expired duties + if expired { + continue; + } + + // Add duty to the map (idempotent) + duties.insert(duty); + + // Update timer if this deadline is earlier + if deadline < curr_deadline { + let (new_duty, new_deadline) = get_curr_duty(&duties, &deadline_func); + curr_duty = new_duty; + curr_deadline = new_deadline; + + let duration = curr_deadline + .signed_duration_since(Utc::now()) + .to_std() + .unwrap_or(FAR_FUTURE_DURATION); + sleep.set(tokio::time::sleep(duration)); + } + } + Err(err) => { + tracing::warn!( + label = %label, + duty = %duty, + error = %err, + "Failed to compute deadline for duty" + ); + let _ = input.response_tx.send(false); + } + Ok(None) => { + // Drop duties that never expire + let _ = input.response_tx.send(false); + } } }