diff --git a/.gitignore b/.gitignore index ad835dbc..9aa40ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ target/ # Code coverage output lcov.info + +.charon* \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 97f1d269..854d4c52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,9 +75,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c97aa0031055a663e364890f2bc15879d6ec38dae9fbeece68fcc82d9cdb81" +checksum = "e502b004e05578e537ce0284843ba3dfaf6a0d5c530f5c20454411aded561289" dependencies = [ "alloy-consensus", "alloy-contract", @@ -98,9 +98,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db5bcdd086f0b1b9610140a12c59b757397be90bd130d8d836fc8da0815a34" +checksum = "3842d8c52fcd3378039f4703dba392dca8b546b1c8ed6183048f8dab95b2be78" dependencies = [ "alloy-primitives", "num_enum", @@ -109,9 +109,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.4.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ea09cffa9ad82f6404e6ab415ea0c41a7674c0f2e2e689cb8683f772b5940d" +checksum = "5c3a590d13de3944675987394715f37537b50b856e3b23a0e66e97d963edbf38" dependencies = [ "alloy-eips", "alloy-primitives", @@ -131,14 +131,14 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-consensus-any" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20736b1f9d927d875d8777ef0c2250d4c57ea828529a9dbfa2c628db57b911e" +checksum = "0f28f769d5ea999f0d8a105e434f483456a15b4e1fcb08edbbbe1650a497ff6d" dependencies = [ "alloy-consensus", "alloy-eips", @@ -150,9 +150,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008aba161fce2a0d94956ae09d7d7a09f8fbdf18acbef921809ef126d6cdaf97" +checksum = "990fa65cd132a99d3c3795a82b9f93ec82b81c7de3bab0bf26ca5c73286f7186" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -167,7 +167,7 @@ dependencies = [ "futures", "futures-util", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -209,7 +209,7 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -234,14 +234,14 @@ dependencies = [ "alloy-rlp", "borsh", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-eips" -version = "1.4.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "691fed81bbafefae0f5a6cedd837ebb3fade46e7d91c5b67a463af12ecf5b11a" +checksum = "09535cbc646b0e0c6fcc12b7597eaed12cf86dff4c4fba9507a61e71b94f30eb" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -257,14 +257,14 @@ dependencies = [ "serde", "serde_with", "sha2", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-genesis" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a838301c4e2546c96db1848f18ffe9f722f2fccd9715b83d4bf269a2cf00b5a1" +checksum = "1005520ccf89fa3d755e46c1d992a9e795466c2e7921be2145ef1f749c5727de" dependencies = [ "alloy-eips", "alloy-primitives", @@ -289,24 +289,24 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60f045b69b5e80b8944b25afe74ae6b974f3044d84b4a7a113da04745b2524cc" +checksum = "72b626409c98ba43aaaa558361bca21440c88fd30df7542c7484b9c7a1489cdb" dependencies = [ "alloy-primitives", "alloy-sol-types", "http", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] [[package]] name = "alloy-network" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b314ed5bdc7f449c53853125af2db5ac4d3954a9f4b205e7d694f02fc1932d1" +checksum = "89924fdcfeee0e0fa42b1f10af42f92802b5d16be614a70897382565663bf7cf" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -325,14 +325,14 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-network-primitives" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9762ac5cca67b0f6ab614f7f8314942eead1c8eeef61511ea43a6ff048dbe0" +checksum = "0f0dbe56ff50065713ff8635d8712a0895db3ad7f209db9793ad8fcb6b1734aa" dependencies = [ "alloy-consensus", "alloy-eips", @@ -371,9 +371,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8f7ca47514e7f552aa9f3f141ab17351332c6637e3bf00462d8e7c5f10f51f" +checksum = "8b56f7a77513308a21a2ba0e9d57785a9d9d2d609e77f4e71a78a1192b83ff2d" dependencies = [ "alloy-chains", "alloy-consensus", @@ -395,13 +395,13 @@ dependencies = [ "either", "futures", "futures-utils-wasm", - "lru 0.16.3", + "lru", "parking_lot", "pin-project", "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -432,9 +432,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26dd083153d2cb73cce1516f5a3f9c3af74764a2761d901581a355777468bd8f" +checksum = "ff01723afc25ec4c5b04de399155bef7b6a96dfde2475492b1b7b4e7a4f46445" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -455,9 +455,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c998214325cfee1fbe61e5abaed3a435f4ca746ac7399b46feb57c364552452" +checksum = "f91bf006bb06b7d812591b6ac33395cb92f46c6a65cda11ee30b348338214f0f" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -467,9 +467,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4a6f49d161ef83354d5ba3c8bc83c8ee464cb90182b215551d5c4b846579be" +checksum = "212ca1c1dab27f531d3858f8b1a2d6bfb2da664be0c1083971078eb7b71abe4b" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -478,9 +478,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11920b16ab7c86052f990dcb4d25312fb2889faf506c4ee13dc946b450536989" +checksum = "5715d0bf7efbd360873518bd9f6595762136b5327a9b759a8c42ccd9b5e44945" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -494,14 +494,14 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-serde" -version = "1.4.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a755a3cc0297683c2879bbfe2ff22778f35068f07444f0b52b5b87570142b6" +checksum = "5ed8531cae8d21ee1c6571d0995f8c9f0652a6ef6452fde369283edea6ab7138" dependencies = [ "alloy-primitives", "serde", @@ -510,9 +510,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea4ac9765e5a7582877ca53688e041fe184880fe75f16edf0945b24a319c710" +checksum = "fb10ccd49d0248df51063fce6b716f68a315dd912d55b32178c883fd48b4021d" dependencies = [ "alloy-primitives", "async-trait", @@ -520,14 +520,14 @@ dependencies = [ "either", "elliptic-curve", "k256", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-signer-local" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9d85b9f7105ab5ce7dae7b0da33cd9d977601a48f759e1c82958978dd1a905" +checksum = "f4d992d44e6c414ece580294abbadb50e74cfd4eaa69787350a4dfd4b20eaa1b" dependencies = [ "alloy-consensus", "alloy-network", @@ -536,7 +536,7 @@ dependencies = [ "async-trait", "k256", "rand 0.8.5", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -614,9 +614,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e72f5c4ba505ebead6a71144d72f21a70beadfb2d84e0a560a985491ecb71de" +checksum = "3f50a9516736d22dd834cc2240e5bf264f338667cc1d9e514b55ec5a78b987ca" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -627,7 +627,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tower", "tracing", @@ -637,9 +637,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "400dc298aaabdbd48be05448c4a19eaa38416c446043f3e54561249149269c32" +checksum = "0a18b541a6197cf9a084481498a766fdf32fefda0c35ea6096df7d511025e9f1" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -668,9 +668,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.4.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17272de4df6b8b59889b264f0306eba47a69f23f57f1c08f1366a4617b48c30" +checksum = "b2289a842d02fe63f8c466db964168bb2c7a9fdfb7b24816dbb17d45520575fb" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -965,7 +965,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -1127,9 +1127,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.15.2" +version = "1.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" dependencies = [ "aws-lc-sys", "zeroize", @@ -1137,9 +1137,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" dependencies = [ "cc", "cmake", @@ -1389,7 +1389,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-util", @@ -1574,9 +1574,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", "jobserver", @@ -1648,7 +1648,7 @@ dependencies = [ "reqwest 0.13.1", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -1675,7 +1675,7 @@ dependencies = [ "hex", "k256", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", ] @@ -1698,7 +1698,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.18", "uuid", ] @@ -1719,7 +1719,7 @@ dependencies = [ "regex", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", ] @@ -1732,7 +1732,7 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1755,7 +1755,7 @@ dependencies = [ "hex", "k256", "sha3", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1767,7 +1767,7 @@ dependencies = [ "k256", "libp2p", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1789,7 +1789,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -1797,6 +1797,28 @@ dependencies = [ "vise-exporter", ] +[[package]] +name = "charon-peerinfo" +version = "1.7.1" +dependencies = [ + "anyhow", + "charon-build-proto", + "chrono", + "clap", + "futures", + "futures-timer", + "hex", + "hex-literal", + "libp2p", + "prost 0.14.3", + "prost-types 0.14.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", + "unsigned-varint 0.8.0", +] + [[package]] name = "charon-relay-server" version = "1.7.1" @@ -1810,7 +1832,7 @@ dependencies = [ "k256", "libp2p", "rand 0.8.5", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -1825,14 +1847,14 @@ dependencies = [ "hex", "k256", "rand_core 0.6.4", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "charon-tracing" version = "1.7.1" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "tracing-loki", @@ -1843,9 +1865,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -2807,21 +2829,20 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fixed-hash" @@ -3196,8 +3217,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -3223,6 +3242,15 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -3250,6 +3278,12 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "hex_fmt" version = "0.3.0" @@ -3275,7 +3309,7 @@ dependencies = [ "rand 0.9.2", "ring", "socket2 0.5.10", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tokio", "tracing", @@ -3298,7 +3332,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] @@ -3839,9 +3873,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -3957,7 +3991,7 @@ dependencies = [ "multiaddr", "pin-project", "rw-stream-sink", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3991,7 +4025,7 @@ dependencies = [ "quick-protobuf-codec", "rand 0.8.5", "rand_core 0.6.4", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "web-time", ] @@ -4026,7 +4060,7 @@ dependencies = [ "quick-protobuf", "rand 0.8.5", "rw-stream-sink", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "unsigned-varint 0.8.0", "web-time", @@ -4034,22 +4068,22 @@ dependencies = [ [[package]] name = "libp2p-dcutr" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4f0eec23bc79cabfdf6934718f161fc42a1d98e2c9d44007c80eb91534200c" +checksum = "2b4107305e12158af3e66960b6181789c547394c9c9a8696f721521602bfc73a" dependencies = [ "asynchronous-codec", "either", "futures", "futures-bounded", "futures-timer", + "hashlink 0.10.0", "libp2p-core", "libp2p-identity", "libp2p-swarm", - "lru 0.12.5", "quick-protobuf", "quick-protobuf-codec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "web-time", ] @@ -4088,7 +4122,7 @@ dependencies = [ "quick-protobuf-codec", "rand 0.8.5", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] @@ -4108,7 +4142,7 @@ dependencies = [ "futures", "futures-timer", "getrandom 0.2.17", - "hashlink", + "hashlink 0.9.1", "hex_fmt", "libp2p-core", "libp2p-identity", @@ -4140,7 +4174,7 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] @@ -4163,7 +4197,7 @@ dependencies = [ "sec1", "serde", "sha2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "zeroize", ] @@ -4190,7 +4224,7 @@ dependencies = [ "serde", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "uint 0.10.0", "web-time", @@ -4267,7 +4301,7 @@ dependencies = [ "rand 0.8.5", "snow", "static_assertions", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "x25519-dalek", "zeroize", @@ -4336,16 +4370,16 @@ dependencies = [ "ring", "rustls", "socket2 0.5.10", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", ] [[package]] name = "libp2p-relay" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551b24ae04c63859bf5e25644acdd6aa469deb5c5cd872ca21c2c9b45a5a5192" +checksum = "d8b9b0392ed623243ad298326b9f806d51191829ac7585cc825c54c6c67b04d9" dependencies = [ "asynchronous-codec", "bytes", @@ -4360,7 +4394,7 @@ dependencies = [ "quick-protobuf-codec", "rand 0.8.5", "static_assertions", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "web-time", ] @@ -4383,7 +4417,7 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec", "rand 0.8.5", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "web-time", ] @@ -4410,19 +4444,19 @@ dependencies = [ [[package]] name = "libp2p-swarm" -version = "0.47.0" +version = "0.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aa762e5215919a34e31c35d4b18bf2e18566ecab7f8a3d39535f4a3068f8b62" +checksum = "ce88c6c4bf746c8482480345ea3edfd08301f49e026889d1cbccfa1808a9ed9e" dependencies = [ "either", "fnv", "futures", "futures-timer", "getrandom 0.2.17", + "hashlink 0.10.0", "libp2p-core", "libp2p-identity", "libp2p-swarm-derive", - "lru 0.12.5", "multistream-select", "rand 0.8.5", "smallvec", @@ -4445,16 +4479,16 @@ dependencies = [ [[package]] name = "libp2p-tcp" -version = "0.44.0" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65b4e030c52c46c8d01559b2b8ca9b7c4185f10576016853129ca1fe5cd1a644" +checksum = "fb6585b9309699f58704ec9ab0bb102eca7a3777170fa91a8678d73ca9cafa93" dependencies = [ "futures", "futures-timer", "if-watch", "libc", "libp2p-core", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tracing", ] @@ -4473,7 +4507,7 @@ dependencies = [ "ring", "rustls", "rustls-webpki", - "thiserror 2.0.17", + "thiserror 2.0.18", "x509-parser", "yasna", ] @@ -4541,7 +4575,7 @@ dependencies = [ "libp2p-identity", "libp2p-webrtc-utils", "send_wrapper 0.6.0", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "wasm-bindgen", "wasm-bindgen-futures", @@ -4563,7 +4597,7 @@ dependencies = [ "pin-project-lite", "rw-stream-sink", "soketto", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "url", "webpki-roots 0.26.11", @@ -4580,7 +4614,7 @@ dependencies = [ "js-sys", "libp2p-core", "send_wrapper 0.6.0", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "wasm-bindgen", "web-sys", @@ -4600,7 +4634,7 @@ dependencies = [ "multiaddr", "multihash", "send_wrapper 0.6.0", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "wasm-bindgen", "wasm-bindgen-futures", @@ -4616,7 +4650,7 @@ dependencies = [ "either", "futures", "libp2p-core", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "yamux 0.12.1", "yamux 0.13.8", @@ -4682,15 +4716,6 @@ dependencies = [ "prost-types 0.13.5", ] -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru" version = "0.16.3" @@ -4719,13 +4744,13 @@ dependencies = [ [[package]] name = "match-lookup" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.114", ] [[package]] @@ -4936,7 +4961,7 @@ dependencies = [ "log", "netlink-packet-core", "netlink-sys", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -5166,7 +5191,7 @@ dependencies = [ "serde_path_to_error", "serde_with", "syn 2.0.114", - "thiserror 2.0.17", + "thiserror 2.0.18", "uuid", "validator", ] @@ -5242,9 +5267,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-probe" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" @@ -5805,7 +5830,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.6.1", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -5827,7 +5852,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -6341,7 +6366,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.0", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", "security-framework 3.5.1", @@ -6358,9 +6383,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -6395,9 +6420,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "aws-lc-rs", "ring", @@ -7118,7 +7143,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tokio-util", @@ -7136,11 +7161,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -7156,9 +7181,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -7683,6 +7708,10 @@ name = "unsigned-varint" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" +dependencies = [ + "futures-io", + "futures-util", +] [[package]] name = "untrusted" @@ -7891,18 +7920,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -7913,11 +7942,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -7926,9 +7956,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7936,9 +7966,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -7949,9 +7979,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -7985,9 +8015,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -8519,9 +8549,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -8563,7 +8593,7 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -8751,6 +8781,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/Cargo.toml b/Cargo.toml index 46da6208..8a162276 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/charon-p2p", "crates/charon-testutil", "crates/tracing", + "crates/peerinfo", "crates/eth2api", "crates/relay-server" ] @@ -34,8 +35,11 @@ cancellation = "0.1.0" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.5.53", features = ["derive", "env", "cargo"] } crossbeam = "0.8.4" +futures = "0.3" +futures-timer = "3.0" backon = "1.6.0" hex = { version = "0.4.3" } +hex-literal = "0.4" prost = "0.14" prost-build = "0.14" prost-types = "0.14" @@ -49,6 +53,7 @@ tokio-util = "0.7.11" libp2p = { version = "0.56", features = ["full", "secp256k1"] } url = "2.5" uuid = { version = "1.19", features = ["serde", "v4"] } +unsigned-varint = { version = "0.8", features = ["futures"] } serde_with = { version = "3.16", features = ["hex", "base64"] } base64 = "0.22" sha3 = "0.10" @@ -80,6 +85,7 @@ charon-p2p = { path = "crates/charon-p2p" } charon-testutil = { path = "crates/charon-testutil" } charon-tracing = { path = "crates/tracing" } charon-relay-server = { path = "crates/relay-server" } +charon-peerinfo = { path = "crates/peerinfo" } eth2api = { path = "crates/eth2api" } [workspace.lints.rust] diff --git a/crates/charon/build.rs b/crates/charon/build.rs index ef2ad4cd..a1db1bae 100644 --- a/crates/charon/build.rs +++ b/crates/charon/build.rs @@ -1,11 +1,10 @@ -//! # Charon Peerinfo Build Script +//! # Charon Build Script //! //! This build script compiles the protobuf files. use std::io::Result; fn main() -> Result<()> { - charon_build_proto::compile_protos("src/peerinfo/peerinfopb/v1")?; charon_build_proto::compile_protos("src/log/loki/lokipb/v1")?; Ok(()) diff --git a/crates/charon/src/lib.rs b/crates/charon/src/lib.rs index 6665d89c..a4033417 100644 --- a/crates/charon/src/lib.rs +++ b/crates/charon/src/lib.rs @@ -4,9 +4,6 @@ //! coordination for Ethereum 2.0 validators. This crate serves as the primary //! entry point for the Charon distributed validator node implementation. -/// Peerinfo. -pub mod peerinfo; - /// Log pub mod log; diff --git a/crates/charon/src/peerinfo/mod.rs b/crates/charon/src/peerinfo/mod.rs deleted file mode 100644 index f2f898d3..00000000 --- a/crates/charon/src/peerinfo/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// Peerinfo protobuf definitions. -pub mod peerinfopb; diff --git a/crates/peerinfo/Cargo.toml b/crates/peerinfo/Cargo.toml new file mode 100644 index 00000000..ae8e6a59 --- /dev/null +++ b/crates/peerinfo/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "charon-peerinfo" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +prost.workspace = true +prost-types.workspace = true +thiserror.workspace = true +libp2p.workspace = true +futures.workspace = true +futures-timer.workspace = true +tracing.workspace = true +chrono.workspace = true +unsigned-varint.workspace = true +hex.workspace = true + +[build-dependencies] +charon-build-proto.workspace = true + +[dev-dependencies] +anyhow.workspace = true +clap.workspace = true +tokio.workspace = true +tracing-subscriber.workspace = true +hex-literal.workspace = true + +[lints] +workspace = true + +[[example]] +name = "peerinfo" diff --git a/crates/peerinfo/build.rs b/crates/peerinfo/build.rs new file mode 100644 index 00000000..d3108e09 --- /dev/null +++ b/crates/peerinfo/build.rs @@ -0,0 +1,11 @@ +//! # Charon Peerinfo Build Script +//! +//! This build script compiles the protobuf files. + +use std::io::Result; + +fn main() -> Result<()> { + charon_build_proto::compile_protos("src/peerinfopb/v1")?; + + Ok(()) +} diff --git a/crates/peerinfo/examples/peerinfo.rs b/crates/peerinfo/examples/peerinfo.rs new file mode 100644 index 00000000..10e919f9 --- /dev/null +++ b/crates/peerinfo/examples/peerinfo.rs @@ -0,0 +1,224 @@ +//! Peerinfo example +//! +//! This example demonstrates the peerinfo protocol by creating two nodes +//! that exchange peer information with each other using mDNS auto-discovery. +//! +//! Run with: +//! ```sh +//! cargo run --example peerinfo -p charon-peerinfo +//! ``` +//! +//! Run two instances on different ports - they will auto-discover each other: +//! +//! Terminal 1: `cargo run --example peerinfo -p charon-peerinfo -- --port 4001` +//! Terminal 2: `cargo run --example peerinfo -p charon-peerinfo -- --port 4002` +#![allow(missing_docs)] +use std::time::Duration; + +use charon_peerinfo::{Behaviour, Config, Event, LocalPeerInfo}; +use clap::Parser; +use libp2p::{ + Multiaddr, Swarm, SwarmBuilder, + futures::StreamExt, + identify, mdns, noise, ping, + swarm::{NetworkBehaviour, SwarmEvent}, + tcp, yamux, +}; +use tokio::signal; +use tracing_subscriber::EnvFilter; + +/// Command line arguments +#[derive(Debug, Parser)] +#[command(name = "peerinfo-example")] +#[command(about = "Demonstrates the peerinfo protocol with mDNS discovery")] +pub struct Args { + /// The port to listen on + #[arg(short, long, default_value = "4001")] + pub port: u16, + + /// Optional address to dial + #[arg(short, long)] + pub dial: Option, + + /// Nickname for this node + #[arg(short, long, default_value = "example-node")] + pub nickname: String, + + /// Peer info exchange interval in seconds + #[arg(short, long, default_value = "5")] + pub interval: u64, +} + +/// Combined behaviour with peerinfo, identify, ping, and mdns +#[derive(NetworkBehaviour)] +pub struct CombinedBehaviour { + pub peer_info: Behaviour, + pub identify: identify::Behaviour, + pub ping: ping::Behaviour, + pub mdns: mdns::tokio::Behaviour, +} + +pub type CombinedEvent = CombinedBehaviourEvent; + +fn build_swarm(peerinfo_config: Config) -> anyhow::Result> { + let swarm = SwarmBuilder::with_new_identity() + .with_tokio() + .with_tcp( + tcp::Config::default(), + noise::Config::new, + yamux::Config::default, + )? + .with_behaviour(|key| { + Ok(CombinedBehaviour { + peer_info: Behaviour::new(peerinfo_config), + identify: identify::Behaviour::new(identify::Config::new( + "/peerinfo-example/1.0.0".to_string(), + key.public(), + )), + ping: ping::Behaviour::new( + ping::Config::new() + .with_interval(Duration::from_secs(15)) + .with_timeout(Duration::from_secs(10)), + ), + mdns: mdns::tokio::Behaviour::new( + mdns::Config::default(), + key.public().to_peer_id(), + )?, + }) + })? + .with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(300))) + .build(); + + Ok(swarm) +} + +fn handle_event(event: SwarmEvent, swarm: &mut Swarm) { + match event { + SwarmEvent::NewListenAddr { address, .. } => { + tracing::info!("Listening on {address}"); + } + SwarmEvent::ConnectionEstablished { + peer_id, endpoint, .. + } => { + tracing::info!( + "Connection established with {peer_id} via {}", + endpoint.get_remote_address() + ); + } + SwarmEvent::ConnectionClosed { peer_id, cause, .. } => { + tracing::info!("Connection closed with {peer_id}: {cause:?}"); + } + SwarmEvent::Behaviour(CombinedEvent::PeerInfo(Event::Received { peer, info, .. })) => { + tracing::info!( + "📥 Received PeerInfo from {peer}:\n\ + │ Version: {}\n\ + │ Git Hash: {}\n\ + │ Nickname: {}\n\ + │ Builder API: {}\n\ + │ Lock Hash: {:?}", + info.charon_version, + info.git_hash, + info.nickname, + info.builder_api_enabled, + hex::encode(&info.lock_hash), + ); + } + SwarmEvent::Behaviour(CombinedEvent::PeerInfo(Event::Error { peer, error, .. })) => { + tracing::warn!("PeerInfo error with {peer}: {error}"); + } + SwarmEvent::Behaviour(CombinedEvent::Identify(identify::Event::Received { + peer_id, + info, + .. + })) => { + tracing::debug!( + "Identify received from {peer_id}: {} {}", + info.protocol_version, + info.agent_version + ); + } + SwarmEvent::Behaviour(CombinedEvent::Ping(ping::Event { peer, result, .. })) => { + match result { + Ok(rtt) => tracing::debug!("Ping to {peer}: {rtt:?}"), + Err(e) => tracing::debug!("Ping to {peer} failed: {e}"), + } + } + SwarmEvent::Behaviour(CombinedEvent::Mdns(mdns::Event::Discovered(peers))) => { + for (peer_id, addr) in peers { + tracing::info!("🔍 mDNS discovered peer {peer_id} at {addr}"); + if let Err(e) = swarm.dial(addr) { + tracing::warn!("Failed to dial discovered peer: {e}"); + } + } + } + SwarmEvent::Behaviour(CombinedEvent::Mdns(mdns::Event::Expired(peers))) => { + for (peer_id, addr) in peers { + tracing::debug!("mDNS peer expired: {peer_id} at {addr}"); + } + } + SwarmEvent::IncomingConnection { local_addr, .. } => { + tracing::debug!("Incoming connection on {local_addr}"); + } + _ => {} + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize logging + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env().add_directive("debug".parse()?)) + .init(); + + let args = Args::parse(); + + // Create local peer info + let local_info = LocalPeerInfo::new( + "v1.0.0", // charon_version + vec![0xDE, 0xAD, 0xBE, 0xEF], // lock_hash (example) + "abc1234", // git_hash + false, // builder_api_enabled + &args.nickname, // nickname + ); + + // Create peerinfo config with custom interval for demonstration + let peerinfo_config = Config::new(local_info) + .with_interval(Duration::from_secs(args.interval)) + .with_timeout(Duration::from_secs(10)); + + let mut swarm = build_swarm(peerinfo_config)?; + + let local_peer_id = *swarm.local_peer_id(); + tracing::info!("Local peer id: {local_peer_id}"); + tracing::info!("mDNS auto-discovery enabled"); + + // Listen on the specified port + let listen_addr: Multiaddr = format!("/ip4/0.0.0.0/tcp/{}", args.port).parse()?; + swarm.listen_on(listen_addr)?; + + // Dial the specified address if provided + if let Some(dial_addr) = &args.dial { + tracing::info!("Dialing {dial_addr}"); + swarm.dial(dial_addr.clone())?; + } + + tracing::info!( + "Peerinfo example started with nickname '{}', interval {}s", + args.nickname, + args.interval + ); + tracing::info!("Press Ctrl+C to exit"); + + // Main event loop + loop { + tokio::select! { + event = swarm.select_next_some() => handle_event(event, &mut swarm), + _ = signal::ctrl_c() => { + tracing::info!("Received Ctrl+C, shutting down..."); + break; + } + } + } + + Ok(()) +} diff --git a/crates/peerinfo/src/behaviour.rs b/crates/peerinfo/src/behaviour.rs new file mode 100644 index 00000000..666e3e3e --- /dev/null +++ b/crates/peerinfo/src/behaviour.rs @@ -0,0 +1,138 @@ +//! NetworkBehaviour implementation for the peerinfo protocol. +//! +//! This behaviour manages peer info exchanges across all connections, +//! emitting events when peer info is received from remote peers. + +use std::{ + collections::VecDeque, + task::{Context, Poll}, +}; + +use libp2p::{ + Multiaddr, PeerId, + swarm::{ + ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, + THandlerOutEvent, ToSwarm, + }, +}; + +use crate::{ + Failure, + config::Config, + handler::{Handler, Success}, + peerinfopb::v1::peerinfo::PeerInfo, +}; + +/// Event emitted by the peerinfo behaviour. +#[derive(Debug, Clone)] +pub enum Event { + /// Received peer info from a remote peer. + Received { + /// The peer that sent the info. + peer: PeerId, + /// The connection on which the info was received. + connection: ConnectionId, + /// The peer info received. + info: PeerInfo, + }, + /// A peer info exchange failed. + Error { + /// The peer with which the exchange failed. + peer: PeerId, + /// The connection on which the exchange failed. + connection: ConnectionId, + /// The failure reason. + error: Failure, + }, +} + +/// Behaviour for the peerinfo protocol. +/// +/// This behaviour periodically exchanges peer info with connected peers +/// and emits events when peer info is received. +pub struct Behaviour { + /// Configuration for the behaviour. + config: Config, + /// Pending events to be emitted. + events: VecDeque, +} + +impl Behaviour { + /// Creates a new [`Behaviour`] with the given configuration. + pub fn new(config: Config) -> Self { + Self { + config, + events: VecDeque::new(), + } + } + + /// Returns the current configuration. + pub fn config(&self) -> &Config { + &self.config + } +} + +impl NetworkBehaviour for Behaviour { + type ConnectionHandler = Handler; + type ToSwarm = Event; + + fn handle_established_inbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _local_addr: &Multiaddr, + _remote_addr: &Multiaddr, + ) -> Result, ConnectionDenied> { + Ok(Handler::new(self.config.clone())) + } + + fn handle_established_outbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _addr: &Multiaddr, + _role_override: libp2p::core::Endpoint, + _port_use: libp2p::core::transport::PortUse, + ) -> Result, ConnectionDenied> { + Ok(Handler::new(self.config.clone())) + } + + fn on_swarm_event(&mut self, _event: FromSwarm) { + // No special handling needed for swarm events + } + + fn on_connection_handler_event( + &mut self, + peer_id: PeerId, + connection_id: ConnectionId, + event: THandlerOutEvent, + ) { + match event { + Ok(Success { peer_info }) => { + self.events.push_back(Event::Received { + peer: peer_id, + connection: connection_id, + info: peer_info, + }); + } + Err(failure) => { + self.events.push_back(Event::Error { + peer: peer_id, + connection: connection_id, + error: failure, + }); + } + } + } + + fn poll( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll>> { + if let Some(event) = self.events.pop_front() { + return Poll::Ready(ToSwarm::GenerateEvent(event)); + } + + Poll::Pending + } +} diff --git a/crates/peerinfo/src/config.rs b/crates/peerinfo/src/config.rs new file mode 100644 index 00000000..86faa208 --- /dev/null +++ b/crates/peerinfo/src/config.rs @@ -0,0 +1,125 @@ +//! Configuration for the peerinfo protocol. + +use std::time::Duration; + +use prost_types::Timestamp; + +use crate::peerinfopb::v1::peerinfo::PeerInfo; + +/// Default interval between peer info exchanges. +const DEFAULT_INTERVAL: Duration = Duration::from_secs(60); + +/// Default timeout for peer info requests. +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(20); + +/// The configuration for the peerinfo protocol. +#[derive(Debug, Clone)] +pub struct Config { + /// The timeout for peer info requests. + pub(crate) timeout: Duration, + /// The interval between peer info exchanges. + pub(crate) interval: Duration, + /// Local peer info to send to other peers. + pub(crate) local_info: LocalPeerInfo, +} + +/// Local peer information to be shared with other peers. +#[derive(Debug, Clone, Default)] +pub struct LocalPeerInfo { + /// Charon version string (e.g., "v1.0.0"). + pub charon_version: String, + /// Lock hash identifying the cluster. + pub lock_hash: Vec, + /// Git commit hash (7 characters). + pub git_hash: String, + /// Whether the builder API is enabled. + pub builder_api_enabled: bool, + /// Human-readable nickname for this peer. + pub nickname: String, + /// Time when the node started. + pub started_at: Option, +} + +impl LocalPeerInfo { + /// Creates a new `LocalPeerInfo` with the given parameters. + pub fn new( + charon_version: impl Into, + lock_hash: impl Into>, + git_hash: impl Into, + builder_api_enabled: bool, + nickname: impl Into, + ) -> Self { + Self { + charon_version: charon_version.into(), + lock_hash: lock_hash.into(), + git_hash: git_hash.into(), + builder_api_enabled, + nickname: nickname.into(), + started_at: Some(Timestamp { + seconds: chrono::Utc::now().timestamp(), + nanos: 0, + }), + } + } + + /// Converts to a protobuf `PeerInfo` message with the current timestamp. + pub(crate) fn to_proto(&self) -> PeerInfo { + let now = chrono::Utc::now(); + PeerInfo { + charon_version: self.charon_version.clone(), + lock_hash: self.lock_hash.clone().into(), + git_hash: self.git_hash.clone(), + sent_at: Some(Timestamp { + seconds: now.timestamp(), + nanos: 0, + }), + started_at: self.started_at, + builder_api_enabled: self.builder_api_enabled, + nickname: self.nickname.clone(), + } + } +} + +impl Config { + /// Creates a new [`Config`] with the following default settings: + /// + /// * [`Config::with_interval`] 60s + /// * [`Config::with_timeout`] 20s + /// + /// These settings have the following effect: + /// + /// * A peer info request is sent every 60 seconds on a healthy connection. + /// * Every request must yield a response within 20 seconds to be + /// successful. + pub fn new(local_info: LocalPeerInfo) -> Self { + Self { + timeout: DEFAULT_TIMEOUT, + interval: DEFAULT_INTERVAL, + local_info, + } + } + + /// Sets the peer info request timeout. + pub fn with_timeout(mut self, d: Duration) -> Self { + self.timeout = d; + self + } + + /// Sets the peer info exchange interval. + pub fn with_interval(mut self, d: Duration) -> Self { + self.interval = d; + self + } + + /// Sets the local peer info. + pub fn with_local_info(mut self, info: LocalPeerInfo) -> Self { + self.local_info = info; + self + } +} + +impl Default for Config { + fn default() -> Self { + Self::new(LocalPeerInfo::default()) + } +} diff --git a/crates/peerinfo/src/failure.rs b/crates/peerinfo/src/failure.rs new file mode 100644 index 00000000..56859ff4 --- /dev/null +++ b/crates/peerinfo/src/failure.rs @@ -0,0 +1,63 @@ +//! Failure types for the peerinfo protocol. + +use std::{error::Error, fmt, sync::Arc}; + +/// A peer info exchange failure. +/// The difference between original `ping` implementation is that it's +/// cloneable. +#[derive(Debug, Clone)] +pub enum Failure { + /// The peer info request timed out, i.e., no response was received within + /// the configured timeout. + Timeout, + /// The peer does not support the peerinfo protocol. + Unsupported, + /// The peer info response was invalid (e.g., missing required fields). + InvalidResponse { + /// Description of the validation error. + reason: String, + }, + /// The peer info exchange failed for reasons other than a timeout. + Other { + /// The underlying error (wrapped in Arc for Clone). + error: Arc, + }, +} + +impl Failure { + /// Creates a new `Failure::Other` from any error type. + pub fn other(e: impl std::error::Error + Send + Sync + 'static) -> Self { + Self::Other { error: Arc::new(e) } + } + + /// Creates a new `Failure::InvalidResponse` with the given reason. + pub fn invalid_response(reason: impl Into) -> Self { + Self::InvalidResponse { + reason: reason.into(), + } + } +} + +impl fmt::Display for Failure { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Failure::Timeout => f.write_str("PeerInfo request timeout"), + Failure::Unsupported => f.write_str("PeerInfo protocol not supported"), + Failure::InvalidResponse { reason } => { + write!(f, "Invalid PeerInfo response: {reason}") + } + Failure::Other { error } => write!(f, "PeerInfo error: {error}"), + } + } +} + +impl Error for Failure { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Failure::Timeout => None, + Failure::Unsupported => None, + Failure::InvalidResponse { .. } => None, + Failure::Other { error } => Some(&**error), + } + } +} diff --git a/crates/peerinfo/src/handler.rs b/crates/peerinfo/src/handler.rs new file mode 100644 index 00000000..79f20509 --- /dev/null +++ b/crates/peerinfo/src/handler.rs @@ -0,0 +1,306 @@ +//! Connection handler for the peerinfo protocol. +//! +//! This handler manages peer info exchanges for a single connection, +//! periodically sending requests and handling incoming requests. +//! +//! The implementation uses libp2p::protocol::ping as a reference + +use std::{ + collections::VecDeque, + convert::Infallible, + task::{Context, Poll}, + time::Duration, +}; + +use futures::{future::BoxFuture, prelude::*}; +use futures_timer::Delay; +use libp2p::{ + core::upgrade::ReadyUpgrade, + swarm::{ + ConnectionHandler, ConnectionHandlerEvent, Stream, StreamProtocol, StreamUpgradeError, + SubstreamProtocol, + handler::{ + ConnectionEvent, DialUpgradeError, FullyNegotiatedInbound, FullyNegotiatedOutbound, + }, + }, +}; + +use crate::{ + PROTOCOL_NAME, config::Config, failure::Failure, peerinfopb::v1::peerinfo::PeerInfo, protocol, +}; + +/// Result of a successful peer info exchange. +#[derive(Debug, Clone)] +pub struct Success { + /// The peer info received from the remote peer. + pub peer_info: PeerInfo, +} + +/// Protocol handler that handles peer info exchange with a remote peer +/// at regular intervals and answers incoming peer info requests. +pub struct Handler { + /// Configuration options. + config: Config, + /// The timer used for the delay to the next request. + interval: Delay, + /// Outbound failures that are pending to be processed by `poll()`. + pending_errors: VecDeque, + /// The number of consecutive failures that occurred. + /// + /// Each successful exchange resets this counter to 0. + failures: u32, + /// The outbound request state. + outbound: Option, + /// The inbound response handler. + inbound: Option, + /// Tracks the state of our handler. + state: State, +} + +/// Tracks the state of the handler. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum State { + /// We are inactive because the other peer doesn't support peerinfo. + Inactive { + /// Whether or not we've reported the missing support yet. + reported: bool, + }, + /// We are actively exchanging peer info. + Active, +} + +impl Handler { + /// Builds a new [`Handler`] with the given configuration. + pub fn new(config: Config) -> Self { + let interval = config.interval; + Handler { + config, + interval: Delay::new(interval), + pending_errors: VecDeque::with_capacity(2), + failures: 0, + outbound: None, + inbound: None, + state: State::Active, + } + } + + fn on_dial_upgrade_error( + &mut self, + DialUpgradeError { error, .. }: DialUpgradeError< + (), + ::OutboundProtocol, + >, + ) { + self.outbound = None; // Request a new substream on the next `poll`. + + // Reset the timer to avoid issues with WASM timer implementation. + // See libp2p/rust-libp2p#5447 for more info. + self.interval.reset(Duration::new(0, 0)); + + let error = match error { + StreamUpgradeError::NegotiationFailed => { + debug_assert_eq!(self.state, State::Active); + self.state = State::Inactive { reported: false }; + return; + } + StreamUpgradeError::Timeout => Failure::other(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "peerinfo protocol negotiation timed out", + )), + StreamUpgradeError::Apply(e) => libp2p::core::util::unreachable(e), + StreamUpgradeError::Io(e) => Failure::other(e), + }; + + self.pending_errors.push_front(error); + } +} + +impl ConnectionHandler for Handler { + type FromBehaviour = Infallible; + type InboundOpenInfo = (); + type InboundProtocol = ReadyUpgrade; + type OutboundOpenInfo = (); + type OutboundProtocol = ReadyUpgrade; + type ToBehaviour = Result; + + fn listen_protocol(&self) -> SubstreamProtocol> { + SubstreamProtocol::new(ReadyUpgrade::new(PROTOCOL_NAME), ()) + } + + fn on_behaviour_event(&mut self, _: Infallible) {} + + #[tracing::instrument(level = "trace", name = "ConnectionHandler::poll", skip(self, cx))] + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll, (), Result>> + { + match self.state { + State::Inactive { reported: true } => { + return Poll::Pending; // Nothing to do on this connection + } + State::Inactive { reported: false } => { + self.state = State::Inactive { reported: true }; + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(Err( + Failure::Unsupported, + ))); + } + State::Active => {} + } + + // Handle inbound requests. + if let Some(fut) = self.inbound.as_mut() { + match fut.poll_unpin(cx) { + Poll::Pending => {} + Poll::Ready(Err(e)) => { + tracing::debug!("Inbound peerinfo error: {:?}", e); + self.inbound = None; + } + Poll::Ready(Ok((stream, _request))) => { + tracing::trace!("Answered inbound peerinfo request from peer"); + self.inbound = + Some(recv_peer_info(stream, self.config.local_info.to_proto()).boxed()); + } + } + } + + loop { + // Check for outbound failures. + if let Some(error) = self.pending_errors.pop_back() { + tracing::debug!("PeerInfo failure: {:?}", error); + + self.failures = self.failures.saturating_add(1); + + // For backward-compatibility, the first failure is "free" and silent. + // This allows peers using new substreams for each request to have + // successful exchanges with peers using a single substream. + if self.failures > 1 { + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(Err(error))); + } + } + + // Continue outbound requests. + match self.outbound.take() { + Some(OutboundState::Request(mut request)) => match request.poll_unpin(cx) { + Poll::Pending => { + self.outbound = Some(OutboundState::Request(request)); + break; + } + Poll::Ready(Ok((stream, peer_info))) => { + self.failures = 0; + self.interval.reset(self.config.interval); + self.outbound = Some(OutboundState::Idle(stream)); + return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(Ok(Success { + peer_info, + }))); + } + Poll::Ready(Err(e)) => { + self.interval.reset(self.config.interval); + self.pending_errors.push_front(e); + } + }, + Some(OutboundState::Idle(stream)) => match self.interval.poll_unpin(cx) { + Poll::Pending => { + self.outbound = Some(OutboundState::Idle(stream)); + break; + } + Poll::Ready(_) => { + self.outbound = Some(OutboundState::Request( + send_peer_info( + stream, + self.config.local_info.to_proto(), + self.config.timeout, + ) + .boxed(), + )); + } + }, + Some(OutboundState::OpenStream) => { + self.outbound = Some(OutboundState::OpenStream); + break; + } + None => match self.interval.poll_unpin(cx) { + Poll::Pending => break, + Poll::Ready(()) => { + self.outbound = Some(OutboundState::OpenStream); + let protocol = SubstreamProtocol::new(ReadyUpgrade::new(PROTOCOL_NAME), ()); + return Poll::Ready(ConnectionHandlerEvent::OutboundSubstreamRequest { + protocol, + }); + } + }, + } + } + + Poll::Pending + } + + fn on_connection_event( + &mut self, + event: ConnectionEvent, + ) { + match event { + ConnectionEvent::FullyNegotiatedInbound(FullyNegotiatedInbound { + protocol: mut stream, + .. + }) => { + stream.ignore_for_keep_alive(); + let local_info = self.config.local_info.to_proto(); + self.inbound = Some(recv_peer_info(stream, local_info).boxed()); + } + ConnectionEvent::FullyNegotiatedOutbound(FullyNegotiatedOutbound { + protocol: mut stream, + .. + }) => { + stream.ignore_for_keep_alive(); + self.interval.reset(Duration::new(0, 0)); + let request = self.config.local_info.to_proto(); + self.outbound = Some(OutboundState::Request( + send_peer_info(stream, request, self.config.timeout).boxed(), + )); + } + ConnectionEvent::DialUpgradeError(dial_upgrade_error) => { + self.on_dial_upgrade_error(dial_upgrade_error) + } + _ => {} + } + } +} + +type RequestFuture = BoxFuture<'static, Result<(Stream, PeerInfo), Failure>>; +type InboundFuture = BoxFuture<'static, Result<(Stream, PeerInfo), std::io::Error>>; + +/// The current state w.r.t. outbound peer info requests. +enum OutboundState { + /// A new substream is being negotiated for the peerinfo protocol. + OpenStream, + /// The stream is idle and waiting for the next request. + Idle(Stream), + /// A request is being sent and the response awaited. + Request(RequestFuture), +} + +/// A wrapper around [`protocol::send_peer_info`] that enforces a timeout. +async fn send_peer_info( + stream: Stream, + request: PeerInfo, + timeout: Duration, +) -> Result<(Stream, PeerInfo), Failure> { + let send = protocol::send_peer_info(stream, &request); + futures::pin_mut!(send); + + match future::select(send, Delay::new(timeout)).await { + future::Either::Left((Ok((stream, response)), _)) => Ok((stream, response)), + future::Either::Left((Err(e), _)) => Err(Failure::other(e)), + future::Either::Right(((), _)) => Err(Failure::Timeout), + } +} + +/// A wrapper around [`protocol::recv_peer_info`] that returns only the stream +/// and request (for use in inbound handling). +async fn recv_peer_info( + stream: Stream, + local_info: PeerInfo, +) -> Result<(Stream, PeerInfo), std::io::Error> { + protocol::recv_peer_info(stream, &local_info).await +} diff --git a/crates/peerinfo/src/lib.rs b/crates/peerinfo/src/lib.rs new file mode 100644 index 00000000..742ad946 --- /dev/null +++ b/crates/peerinfo/src/lib.rs @@ -0,0 +1,67 @@ +//! # Charon Peerinfo +//! +//! The peerinfo protocol enables Charon nodes to exchange metadata about +//! themselves with connected peers. This includes version information, +//! cluster lock hash, git commit, builder API status, and nicknames. +//! +//! ## Protocol Overview +//! +//! The protocol works as a request-response pattern: +//! 1. Each peer periodically sends its own `PeerInfo` to connected peers +//! 2. The receiving peer responds with its own `PeerInfo` +//! 3. Both peers can use this information to verify compatibility and track +//! peer metadata +//! +//! ## Usage +//! +//! ```rust,ignore +//! use charon_peerinfo::{Behaviour, Config, LocalPeerInfo}; +//! +//! let local_info = LocalPeerInfo::new( +//! "v1.0.0", +//! vec![0u8; 32], // lock hash +//! "abc1234", // git hash +//! false, // builder API enabled +//! "my-node", // nickname +//! ); +//! +//! let config = Config::new(local_info) +//! .with_interval(Duration::from_secs(60)) +//! .with_timeout(Duration::from_secs(20)); +//! +//! let behaviour = Behaviour::new(config); +//! ``` + +use libp2p::swarm::StreamProtocol; + +/// Behaviour implementation for the peerinfo protocol. +pub mod behaviour; + +/// Configuration for the peerinfo protocol. +pub mod config; + +/// Failure types for the peerinfo protocol. +pub mod failure; + +/// Connection handler for the peerinfo protocol. +pub mod handler; + +/// Peerinfo protobuf definitions. +pub mod peerinfopb; + +/// Wire protocol implementation. +pub mod protocol; + +// Re-exports for convenience +pub use behaviour::{Behaviour, Event}; +pub use config::{Config, LocalPeerInfo}; +pub use failure::Failure; +pub use handler::Success; + +/// The protocol name for the peerinfo protocol (version 2.0.0). +pub const PROTOCOL_NAME: StreamProtocol = StreamProtocol::new("/charon/peerinfo/2.0.0"); + +/// Returns the supported protocols of this package in order of precedence. +pub fn protocols() -> Vec { + vec![PROTOCOL_NAME] +} diff --git a/crates/charon/src/peerinfo/peerinfopb/mod.rs b/crates/peerinfo/src/peerinfopb/mod.rs similarity index 100% rename from crates/charon/src/peerinfo/peerinfopb/mod.rs rename to crates/peerinfo/src/peerinfopb/mod.rs diff --git a/crates/charon/src/peerinfo/peerinfopb/v1.rs b/crates/peerinfo/src/peerinfopb/v1.rs similarity index 100% rename from crates/charon/src/peerinfo/peerinfopb/v1.rs rename to crates/peerinfo/src/peerinfopb/v1.rs diff --git a/crates/charon/src/peerinfo/peerinfopb/v1/peerinfo.proto b/crates/peerinfo/src/peerinfopb/v1/peerinfo.proto similarity index 100% rename from crates/charon/src/peerinfo/peerinfopb/v1/peerinfo.proto rename to crates/peerinfo/src/peerinfopb/v1/peerinfo.proto diff --git a/crates/charon/src/peerinfo/peerinfopb/v1/peerinfo.rs b/crates/peerinfo/src/peerinfopb/v1/peerinfo.rs similarity index 100% rename from crates/charon/src/peerinfo/peerinfopb/v1/peerinfo.rs rename to crates/peerinfo/src/peerinfopb/v1/peerinfo.rs diff --git a/crates/peerinfo/src/protocol.rs b/crates/peerinfo/src/protocol.rs new file mode 100644 index 00000000..5516b5a5 --- /dev/null +++ b/crates/peerinfo/src/protocol.rs @@ -0,0 +1,436 @@ +//! Wire protocol implementation for the peerinfo protocol. +//! +//! This module handles encoding and decoding of PeerInfo messages on the wire +//! using the same format as Go's libp2p pbio package: +//! +//! ```text +//! [unsigned varint length][protobuf bytes] +//! ``` +//! +//! The unsigned varint encoding uses 7 bits per byte for data, with the MSB +//! as a continuation flag (1 = more bytes follow, 0 = last byte). + +use std::io; + +use futures::prelude::*; +use libp2p::swarm::Stream; +use prost::Message; +use unsigned_varint::aio::read_usize; + +use crate::peerinfopb::v1::peerinfo::PeerInfo; + +/// Maximum message size (64KB should be plenty for peer info). +const MAX_MESSAGE_SIZE: usize = 64 * 1024; + +/// Writes a protobuf message with unsigned varint length prefix to the stream. +/// +/// Wire format: `[uvarint length][protobuf bytes]` +async fn write_protobuf( + stream: &mut S, + msg: &M, +) -> io::Result<()> { + // Encode message to protobuf bytes + let mut buf = Vec::with_capacity(msg.encoded_len()); + msg.encode(&mut buf) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + + // Write unsigned varint length prefix + let mut len_buf = unsigned_varint::encode::usize_buffer(); + let encoded_len = unsigned_varint::encode::usize(buf.len(), &mut len_buf); + stream.write_all(encoded_len).await?; + + // Write protobuf bytes + stream.write_all(&buf).await?; + stream.flush().await +} + +/// Reads a protobuf message with unsigned varint length prefix from the stream. +/// +/// Wire format: `[uvarint length][protobuf bytes]` +/// +/// Returns an error if the message exceeds `MAX_MESSAGE_SIZE`. +async fn read_protobuf( + stream: &mut S, +) -> io::Result { + // Read unsigned varint length prefix + let msg_len = read_usize(&mut *stream).await.map_err(|e| match e { + unsigned_varint::io::ReadError::Io(io_err) => io_err, + other => io::Error::new(io::ErrorKind::InvalidData, other), + })?; + + if msg_len > MAX_MESSAGE_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("message too large: {msg_len} bytes (max: {MAX_MESSAGE_SIZE})"), + )); + } + + // Read exactly `msg_len` protobuf bytes + let mut buf = vec![0u8; msg_len]; + stream.read_exact(&mut buf).await?; + + // Unmarshal protobuf + M::decode(&buf[..]).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) +} + +/// Sends a peer info request and waits for a response. +/// +/// Returns the response `PeerInfo` on success. +pub async fn send_peer_info( + mut stream: Stream, + request: &PeerInfo, +) -> io::Result<(Stream, PeerInfo)> { + write_protobuf(&mut stream, request).await?; + let response = read_protobuf(&mut stream).await?; + Ok((stream, response)) +} + +/// Receives a peer info request and sends a response. +/// +/// Returns the stream for potential reuse after successfully responding. +pub async fn recv_peer_info( + mut stream: Stream, + local_info: &PeerInfo, +) -> io::Result<(Stream, PeerInfo)> { + let request = read_protobuf(&mut stream).await?; + write_protobuf(&mut stream, local_info).await?; + Ok((stream, request)) +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + // Test case: minimal + // CharonVersion: "v1.0.0" + // LockHash: deadbeef + // BuilderApiEnabled: false + const PEERINFO_MINIMAL: &[u8] = &hex!("0a0676312e302e301204deadbeef"); + + // Test case: with_git_hash + // CharonVersion: "v1.7.1" + // LockHash: 0000000000000000000000000000000000000000000000000000000000000000 + // GitHash: "abc1234" + // BuilderApiEnabled: false + const PEERINFO_WITH_GIT_HASH: &[u8] = &hex!( + "0a0676312e372e3112200000000000000000000000000000000000000000000000000000000000000000220761626331323334" + ); + + // Test case: full + // CharonVersion: "v1.7.1" + // LockHash: 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20 + // SentAt: 2025-01-15T12:30:45Z + // GitHash: "a1b2c3d" + // StartedAt: 2025-01-15T10:00:00Z + // BuilderApiEnabled: true + // Nickname: "test-node" + const PEERINFO_FULL: &[u8] = &hex!( + "0a0676312e372e3112200102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f201a0608f5d49ebc062207613162326333642a0608a08e9ebc0630013a09746573742d6e6f6465" + ); + + // Test case: builder_disabled + // CharonVersion: "v1.5.0" + // LockHash: ffffffff + // SentAt: 2024-12-01T00:00:00Z + // GitHash: "1234567" + // StartedAt: 2024-11-30T23:00:00Z + // BuilderApiEnabled: false + // Nickname: "validator-1" + const PEERINFO_BUILDER_DISABLED: &[u8] = &hex!( + "0a0676312e352e301204ffffffff1a060880ceaeba062207313233343536372a0608f0b1aeba063a0b76616c696461746f722d31" + ); + + // Test case: empty_optional_fields + // CharonVersion: "v1.6.0" + // LockHash: cafebabe + // BuilderApiEnabled: false + const PEERINFO_EMPTY_OPTIONAL_FIELDS: &[u8] = &hex!("0a0676312e362e301204cafebabe"); + + /// Helper to create a PeerInfo with minimal fields + fn make_minimal_peerinfo() -> PeerInfo { + PeerInfo { + charon_version: "v1.0.0".to_string(), + lock_hash: vec![0xde, 0xad, 0xbe, 0xef].into(), + sent_at: None, + git_hash: String::new(), + started_at: None, + builder_api_enabled: false, + nickname: String::new(), + } + } + + /// Helper to create a PeerInfo with git hash + fn make_with_git_hash_peerinfo() -> PeerInfo { + PeerInfo { + charon_version: "v1.7.1".to_string(), + lock_hash: vec![0u8; 32].into(), + sent_at: None, + git_hash: "abc1234".to_string(), + started_at: None, + builder_api_enabled: false, + nickname: String::new(), + } + } + + /// Helper to create a full PeerInfo with all fields + fn make_full_peerinfo() -> PeerInfo { + PeerInfo { + charon_version: "v1.7.1".to_string(), + lock_hash: (1u8..=32).collect::>().into(), + sent_at: Some(prost_types::Timestamp { + seconds: 1736944245, // 2025-01-15T13:00:45Z + nanos: 0, + }), + git_hash: "a1b2c3d".to_string(), + started_at: Some(prost_types::Timestamp { + seconds: 1736935200, // 2025-01-15T10:30:00Z + nanos: 0, + }), + builder_api_enabled: true, + nickname: "test-node".to_string(), + } + } + + /// Helper to create a PeerInfo with builder disabled + fn make_builder_disabled_peerinfo() -> PeerInfo { + PeerInfo { + charon_version: "v1.5.0".to_string(), + lock_hash: vec![0xff, 0xff, 0xff, 0xff].into(), + sent_at: Some(prost_types::Timestamp { + seconds: 1733011200, // 2024-12-01T00:00:00Z + nanos: 0, + }), + git_hash: "1234567".to_string(), + started_at: Some(prost_types::Timestamp { + seconds: 1733007600, // 2024-11-30T23:00:00Z + nanos: 0, + }), + builder_api_enabled: false, + nickname: "validator-1".to_string(), + } + } + + /// Helper to create a PeerInfo with empty optional fields + fn make_empty_optional_peerinfo() -> PeerInfo { + PeerInfo { + charon_version: "v1.6.0".to_string(), + lock_hash: vec![0xca, 0xfe, 0xba, 0xbe].into(), + sent_at: None, + git_hash: String::new(), + started_at: None, + builder_api_enabled: false, + nickname: String::new(), + } + } + + #[test] + fn test_decode_minimal() { + let decoded = PeerInfo::decode(PEERINFO_MINIMAL).unwrap(); + let expected = make_minimal_peerinfo(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_decode_with_git_hash() { + let decoded = PeerInfo::decode(PEERINFO_WITH_GIT_HASH).unwrap(); + let expected = make_with_git_hash_peerinfo(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_decode_full() { + let decoded = PeerInfo::decode(PEERINFO_FULL).unwrap(); + let expected = make_full_peerinfo(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_decode_builder_disabled() { + let decoded = PeerInfo::decode(PEERINFO_BUILDER_DISABLED).unwrap(); + let expected = make_builder_disabled_peerinfo(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_decode_empty_optional_fields() { + let decoded = PeerInfo::decode(PEERINFO_EMPTY_OPTIONAL_FIELDS).unwrap(); + let expected = make_empty_optional_peerinfo(); + assert_eq!(decoded, expected); + } + + #[test] + fn test_encode_minimal() { + let msg = make_minimal_peerinfo(); + let mut buf = Vec::new(); + msg.encode(&mut buf).unwrap(); + assert_eq!(buf, PEERINFO_MINIMAL); + } + + #[test] + fn test_encode_with_git_hash() { + let msg = make_with_git_hash_peerinfo(); + let mut buf = Vec::new(); + msg.encode(&mut buf).unwrap(); + assert_eq!(buf, PEERINFO_WITH_GIT_HASH); + } + + #[test] + fn test_encode_full() { + let msg = make_full_peerinfo(); + let mut buf = Vec::new(); + msg.encode(&mut buf).unwrap(); + assert_eq!(buf, PEERINFO_FULL); + } + + #[test] + fn test_encode_builder_disabled() { + let msg = make_builder_disabled_peerinfo(); + let mut buf = Vec::new(); + msg.encode(&mut buf).unwrap(); + assert_eq!(buf, PEERINFO_BUILDER_DISABLED); + } + + #[test] + fn test_encode_empty_optional_fields() { + let msg = make_empty_optional_peerinfo(); + let mut buf = Vec::new(); + msg.encode(&mut buf).unwrap(); + assert_eq!(buf, PEERINFO_EMPTY_OPTIONAL_FIELDS); + } + + #[test] + fn test_roundtrip_all_variants() { + let variants = [ + make_minimal_peerinfo(), + make_with_git_hash_peerinfo(), + make_full_peerinfo(), + make_builder_disabled_peerinfo(), + make_empty_optional_peerinfo(), + ]; + + for original in variants { + let mut buf = Vec::new(); + original.encode(&mut buf).unwrap(); + let decoded = PeerInfo::decode(&buf[..]).unwrap(); + assert_eq!(original, decoded); + } + } + + #[tokio::test] + async fn test_write_read_protobuf_minimal() { + let original = make_minimal_peerinfo(); + + // Write to a cursor + let mut buf = Vec::new(); + write_protobuf(&mut buf, &original).await.unwrap(); + + // The wire format should be: [varint length][protobuf bytes] + // Minimal message is 14 bytes, so length prefix is just 1 byte (14 < 128) + assert_eq!(buf[0] as usize, PEERINFO_MINIMAL.len()); + assert_eq!(&buf[1..], PEERINFO_MINIMAL); + + // Read it back + let mut cursor = futures::io::Cursor::new(&buf[..]); + let decoded: PeerInfo = read_protobuf(&mut cursor).await.unwrap(); + assert_eq!(original, decoded); + } + + #[tokio::test] + async fn test_write_read_protobuf_full() { + let original = make_full_peerinfo(); + + let mut buf = Vec::new(); + write_protobuf(&mut buf, &original).await.unwrap(); + + // Read it back + let mut cursor = futures::io::Cursor::new(&buf[..]); + let decoded: PeerInfo = read_protobuf(&mut cursor).await.unwrap(); + assert_eq!(original, decoded); + } + + #[tokio::test] + async fn test_write_read_protobuf_all_variants() { + let variants = [ + make_minimal_peerinfo(), + make_with_git_hash_peerinfo(), + make_full_peerinfo(), + make_builder_disabled_peerinfo(), + make_empty_optional_peerinfo(), + ]; + + for original in variants { + let mut buf = Vec::new(); + write_protobuf(&mut buf, &original).await.unwrap(); + + let mut cursor = futures::io::Cursor::new(&buf[..]); + let decoded: PeerInfo = read_protobuf(&mut cursor).await.unwrap(); + assert_eq!(original, decoded); + } + } + + #[tokio::test] + async fn test_read_protobuf_message_too_large() { + // Create a buffer with a length prefix that exceeds MAX_MESSAGE_SIZE + let mut buf = Vec::new(); + let large_len = MAX_MESSAGE_SIZE + 1; + let mut len_buf = unsigned_varint::encode::usize_buffer(); + let encoded_len = unsigned_varint::encode::usize(large_len, &mut len_buf); + buf.extend_from_slice(encoded_len); + + let mut cursor = futures::io::Cursor::new(&buf[..]); + let result: io::Result = read_protobuf(&mut cursor).await; + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + assert!(err.to_string().contains("message too large")); + } + + #[tokio::test] + async fn test_read_protobuf_invalid_data() { + // Create a buffer with valid length but invalid protobuf data + let invalid_data = [0x05, 0xff, 0xff, 0xff, 0xff, 0xff]; // length 5, then garbage + + let mut cursor = futures::io::Cursor::new(&invalid_data[..]); + let result: io::Result = read_protobuf(&mut cursor).await; + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData); + } + + #[tokio::test] + async fn test_read_protobuf_truncated_message() { + // Create a buffer that claims a length but doesn't have enough bytes + let truncated = [0x10]; // claims 16 bytes but has none + + let mut cursor = futures::io::Cursor::new(&truncated[..]); + let result: io::Result = read_protobuf(&mut cursor).await; + + assert!(result.is_err()); + assert_eq!(result.unwrap_err().kind(), io::ErrorKind::UnexpectedEof); + } + + #[tokio::test] + async fn test_multiple_messages_in_stream() { + let msg1 = make_minimal_peerinfo(); + let msg2 = make_full_peerinfo(); + let msg3 = make_with_git_hash_peerinfo(); + + // Write multiple messages to the same buffer + let mut buf = Vec::new(); + write_protobuf(&mut buf, &msg1).await.unwrap(); + write_protobuf(&mut buf, &msg2).await.unwrap(); + write_protobuf(&mut buf, &msg3).await.unwrap(); + + // Read them back in order + let mut cursor = futures::io::Cursor::new(&buf[..]); + let decoded1: PeerInfo = read_protobuf(&mut cursor).await.unwrap(); + let decoded2: PeerInfo = read_protobuf(&mut cursor).await.unwrap(); + let decoded3: PeerInfo = read_protobuf(&mut cursor).await.unwrap(); + + assert_eq!(msg1, decoded1); + assert_eq!(msg2, decoded2); + assert_eq!(msg3, decoded3); + } +}