diff --git a/Cargo.lock b/Cargo.lock index 52da10d..c196d3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -115,12 +124,37 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "ascii" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -132,18 +166,67 @@ dependencies = [ "syn", ] +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http", + "log", + "url", +] + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + [[package]] name = "base64" version = "0.21.7" @@ -179,9 +262,23 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] [[package]] name = "block-buffer" @@ -192,6 +289,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -245,9 +351,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -267,6 +373,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "chunked_transfer" version = "1.5.0" @@ -375,6 +493,37 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cordyceps" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +dependencies = [ + "loom", + "tracing", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -385,6 +534,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -561,6 +720,21 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -636,6 +810,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "211f05e03c7d03754740fd9e585de910a095d6b99f8bcfffdef8319fa02a8331" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctrlc" version = "3.5.1" @@ -656,13 +839,31 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "digest", - "fiat-crypto", + "digest 0.10.7", + "fiat-crypto 0.2.9", "rustc_version", "subtle", "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.11.0-rc.10", + "fiat-crypto 0.3.0", + "rand_core 0.9.5", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek-derive" version = "0.1.1" @@ -674,14 +875,38 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", ] [[package]] @@ -697,17 +922,34 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", "quote", "syn", ] +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + [[package]] name = "debugid" version = "0.8.0" @@ -723,18 +965,109 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "zeroize", ] +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa94b64bfc6549e6e4b5a3216f22593224174083da7a90db47e951c4fb31725" +dependencies = [ + "block-buffer 0.11.0", + "const-oid 0.10.2", + "crypto-common 0.2.0", ] [[package]] @@ -785,11 +1118,24 @@ dependencies = [ name = "distribution" version = "0.1.0" dependencies = [ - "ed25519-dalek", + "ed25519-dalek 2.2.0", + "iroh", "rand_core 0.6.4", "serde", "serde_json", "swactor", + "tokio", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", ] [[package]] @@ -802,14 +1148,40 @@ dependencies = [ "serde_json", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ed25519" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519" +version = "3.0.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" +dependencies = [ + "pkcs8 0.11.0-rc.11", + "serde", + "signature 3.0.0-rc.10", ] [[package]] @@ -818,11 +1190,27 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek", - "ed25519", + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", "rand_core 0.6.4", "serde", - "sha2", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" +dependencies = [ + "curve25519-dalek 5.0.0-pre.1", + "ed25519 3.0.0-rc.4", + "rand_core 0.9.5", + "serde", + "sha2 0.11.0-rc.2", + "signature 3.0.0-rc.10", "subtle", "zeroize", ] @@ -854,6 +1242,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enum-assoc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -876,6 +1287,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand", + "siphasher", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -888,6 +1311,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -906,6 +1335,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -930,6 +1365,34 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-buffered" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.0", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -946,12 +1409,47 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -970,8 +1468,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1002,6 +1502,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1019,8 +1534,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1030,9 +1547,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1059,6 +1578,18 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "h2" version = "0.4.13" @@ -1089,6 +1620,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1106,7 +1646,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", "serde", ] @@ -1115,6 +1655,25 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] [[package]] name = "heck" @@ -1128,6 +1687,59 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "h2", + "http", + "idna", + "ipnet", + "once_cell", + "rand", + "ring", + "rustls", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand", + "resolv-conf", + "rustls", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tracing", +] + [[package]] name = "http" version = "1.4.0" @@ -1173,6 +1785,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -1187,6 +1808,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1209,6 +1831,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -1244,7 +1867,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.2", "system-configuration", "tokio", "tower-service", @@ -1252,6 +1875,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -1345,6 +1992,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + [[package]] name = "idna" version = "1.1.0" @@ -1366,6 +2019,27 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "igd-next" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand", + "tokio", + "url", + "xmltree", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -1393,13 +2067,25 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" dependencies = [ - "darling", + "darling 0.23.0", "indoc", "proc-macro2", "quote", "syn", ] +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1416,6 +2102,213 @@ dependencies = [ "serde", ] +[[package]] +name = "iroh" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5236da4d5681f317ec393c8fe2b7e3d360d31c6bb40383991d0b7429ca5ad117" +dependencies = [ + "backon", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more", + "ed25519-dalek 3.0.0-pre.1", + "futures-util", + "getrandom 0.3.4", + "hickory-resolver", + "http", + "igd-next", + "iroh-base", + "iroh-metrics", + "iroh-quinn", + "iroh-quinn-proto", + "iroh-quinn-udp", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netwatch", + "papaya", + "pin-project", + "pkarr", + "pkcs8 0.11.0-rc.11", + "portmapper", + "rand", + "reqwest", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-webpki", + "serde", + "smallvec", + "strum 0.27.2", + "sync_wrapper", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", + "webpki-roots", +] + +[[package]] +name = "iroh-base" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c99d836a1c99e037e98d1bf3ef209c3a4df97555a00ce9510eb78eccdf5567" +dependencies = [ + "curve25519-dalek 5.0.0-pre.1", + "data-encoding", + "derive_more", + "digest 0.11.0-rc.10", + "ed25519-dalek 3.0.0-pre.1", + "n0-error", + "rand_core 0.9.5", + "serde", + "sha2 0.11.0-rc.2", + "url", + "zeroize", + "zeroize_derive", +] + +[[package]] +name = "iroh-metrics" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c946095f060e6e59b9ff30cc26c75cdb758e7fb0cde8312c89e2144654989fcb" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "postcard", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab063c2bfd6c3d5a33a913d4fdb5252f140db29ec67c704f20f3da7e8f92dbf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "iroh-quinn" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034ed21f34c657a123d39525d948c885aacba59508805e4dd67d71f022e7151b" +dependencies = [ + "bytes", + "cfg_aliases", + "iroh-quinn-proto", + "iroh-quinn-udp", + "pin-project-lite", + "rustc-hash", + "rustls", + "socket2 0.6.2", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "web-time 1.1.0", +] + +[[package]] +name = "iroh-quinn-proto" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de99ad8adc878ee0e68509ad256152ce23b8bbe45f5539d04e179630aca40a9" +dependencies = [ + "bytes", + "derive_more", + "enum-assoc", + "fastbloom", + "getrandom 0.3.4", + "identity-hash", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "sorted-index-buffer", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time 1.1.0", +] + +[[package]] +name = "iroh-quinn-udp" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f981dadd5a072a9e0efcd24bdcc388e570073f7e51b33505ceb1ef4668c80c86" +dependencies = [ + "cfg_aliases", + "libc", + "socket2 0.6.2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "iroh-relay" +version = "0.96.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd2b63e654b9dec799a73372cdc79b529ca6c7248c0c8de7da78a02e3a46f03c" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more", + "getrandom 0.3.4", + "hickory-resolver", + "http", + "http-body-util", + "hyper", + "hyper-util", + "iroh-base", + "iroh-metrics", + "iroh-quinn", + "iroh-quinn-proto", + "lru 0.16.3", + "n0-error", + "n0-future", + "num_enum", + "pin-project", + "pkarr", + "postcard", + "rand", + "reqwest", + "rustls", + "rustls-pki-types", + "serde", + "serde_bytes", + "strum 0.27.2", + "tokio", + "tokio-rustls", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "vergen-gitcl", + "webpki-roots", + "ws_stream_wasm", + "z32", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -1526,9 +2419,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.181" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" @@ -1564,6 +2457,12 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1579,6 +2478,19 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lru" version = "0.12.5" @@ -1588,6 +2500,27 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + [[package]] name = "mach2" version = "0.4.3" @@ -1597,6 +2530,15 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1640,10 +2582,80 @@ dependencies = [ ] [[package]] -name = "native-tls" -version = "0.2.15" +name = "moka" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cdede44f9a69cab2899a2049e2c3bd49bf911a157f6a3353d4a91c61abbce44" +checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "n0-error" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4782b4baf92d686d161c15460c83d16ebcfd215918763903e9619842665cae" +dependencies = [ + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03755949235714b2b307e5ae89dd8c1c2531fb127d9b8b7b4adf9c876cd3ed18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "n0-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" +dependencies = [ + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time 1.1.0", +] + +[[package]] +name = "n0-watcher" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38795f7932e6e9d1c6e989270ef5b3ff24ebb910e2c9d4bed2d28d8bae3007dc" +dependencies = [ + "derive_more", + "n0-error", + "n0-future", +] + +[[package]] +name = "native-tls" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5d26952a508f321b4d3d2e80e78fc2603eaefcdf0c30783867f19586518bdc" dependencies = [ "libc", "log", @@ -1656,6 +2668,124 @@ dependencies = [ "tempfile", ] +[[package]] +name = "netdev" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9815643a243856e7bd84524e1ff739e901e846cfb06ad9627cd2b6d59bd737" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "libc", + "mac-addr", + "netlink-packet-core", + "netlink-packet-route 0.25.1", + "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.59.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-packet-route" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "netwatch" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "454b8c0759b2097581f25ed5180b4a1d14c324fde6d0734932a288e044d06232" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more", + "iroh-quinn-udp", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netlink-packet-core", + "netlink-packet-route 0.28.0", + "netlink-proto", + "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2 0.6.2", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows", + "windows-result", + "wmi", +] + [[package]] name = "nix" version = "0.30.1" @@ -1668,6 +2798,21 @@ dependencies = [ "libc", ] +[[package]] +name = "ntimestamp" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50f94c405726d3e0095e89e72f75ce7f6587b94a8bd8dc8054b73f65c0fd68c" +dependencies = [ + "base32", + "document-features", + "getrandom 0.2.17", + "httpdate", + "js-sys", + "once_cell", + "serde", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1677,6 +2822,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + [[package]] name = "num-traits" version = "0.2.19" @@ -1686,6 +2837,37 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc2" version = "0.6.3" @@ -1695,12 +2877,50 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "block2", + "dispatch2", + "libc", + "objc2", +] + [[package]] name = "objc2-encode" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + [[package]] name = "object" version = "0.36.7" @@ -1727,6 +2947,10 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -1768,9 +2992,9 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" @@ -1784,6 +3008,22 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "papaya" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f" +dependencies = [ + "equivalent", + "seize", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1813,12 +3053,51 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1831,14 +3110,55 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkarr" +version = "5.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d346b545765a0ef58b6a7e160e17ddaa7427f439b7b9a287df6c88c9e04bf2" +dependencies = [ + "async-compat", + "base32", + "bytes", + "cfg_aliases", + "document-features", + "dyn-clone", + "ed25519-dalek 3.0.0-pre.1", + "futures-buffered", + "futures-lite", + "getrandom 0.3.4", + "log", + "lru 0.16.3", + "ntimestamp", + "reqwest", + "self_cell", + "serde", + "sha1_smol", + "simple-dns", + "thiserror 2.0.18", + "tokio", + "tracing", + "url", + "wasm-bindgen-futures", +] + [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +dependencies = [ + "der 0.8.0", + "spki 0.8.0-rc.4", ] [[package]] @@ -1847,6 +3167,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "plotters" version = "0.3.7" @@ -1881,6 +3214,36 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portmapper" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d2a8825353ace3285138da3378b1e21860d60351942f7aa3b99b13b41f80318" +dependencies = [ + "base64 0.22.1", + "bytes", + "derive_more", + "futures-lite", + "futures-util", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "netwatch", + "num_enum", + "rand", + "serde", + "smallvec", + "socket2 0.6.2", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", +] + [[package]] name = "postcard" version = "1.1.3" @@ -1890,9 +3253,22 @@ dependencies = [ "cobs", "embedded-io 0.4.0", "embedded-io 0.6.1", + "heapless", + "postcard-derive", "serde", ] +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1902,6 +3278,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1921,6 +3303,15 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2057,6 +3448,70 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time 1.1.0", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time 1.1.0", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.44" @@ -2132,9 +3587,9 @@ dependencies = [ "indoc", "instability", "itertools 0.13.0", - "lru", + "lru 0.12.5", "paste", - "strum", + "strum 0.26.3", "unicode-segmentation", "unicode-truncate", "unicode-width 0.2.0", @@ -2249,6 +3704,8 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -2256,15 +3713,25 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", + "webpki-roots", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "ring" version = "0.17.14" @@ -2288,6 +3755,7 @@ dependencies = [ "crossterm", "ctrlc", "distribution", + "iroh", "ratatui", "serde", "serde_json", @@ -2350,7 +3818,9 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -2363,6 +3833,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time 1.1.0", "zeroize", ] @@ -2419,6 +3890,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -2427,12 +3904,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2440,14 +3917,30 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" dependencies = [ "core-foundation-sys", "libc", ] +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "1.0.27" @@ -2458,6 +3951,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.228" @@ -2468,6 +3967,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -2522,6 +4031,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -2530,7 +4045,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e3878ab0f98e35b2df35fe53201d088299b41a6bb63e3e34dada2ac4abd924" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-rc.10", ] [[package]] @@ -2588,6 +4114,27 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple-dns" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" +dependencies = [ + "bitflags", +] + [[package]] name = "simulation" version = "0.1.0" @@ -2603,6 +4150,12 @@ dependencies = [ "toml", ] +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -2618,6 +4171,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.2" @@ -2628,6 +4191,38 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "spki" version = "0.7.3" @@ -2635,7 +4230,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +dependencies = [ + "base64ct", + "der 0.8.0", ] [[package]] @@ -2668,7 +4273,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -2684,6 +4298,18 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2703,7 +4329,7 @@ dependencies = [ "serde", "swactor-std", "tracing", - "web-time", + "web-time 0.2.4", ] [[package]] @@ -2763,7 +4389,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2777,6 +4403,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "target-lexicon" version = "0.12.16" @@ -2860,6 +4492,40 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "js-sys", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny_http" version = "0.12.0" @@ -2892,6 +4558,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.49.0" @@ -2902,10 +4583,23 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "socket2", + "signal-hook-registry", + "socket2 0.6.2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -2926,6 +4620,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -2935,10 +4641,33 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] +[[package]] +name = "tokio-websockets" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b6348ebfaaecd771cecb69e832961d277f59845d4220a584701f72728152b7" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.3.4", + "http", + "httparse", + "rand", + "ring", + "rustls-pki-types", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + [[package]] name = "toml" version = "0.8.23" @@ -2947,8 +4676,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -2960,6 +4689,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -2969,11 +4707,32 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" @@ -3031,6 +4790,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3074,10 +4834,14 @@ version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -3174,6 +4938,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -3190,10 +4955,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ + "getrandom 0.4.1", "js-sys", "wasm-bindgen", ] @@ -3210,6 +4976,54 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vergen" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "vergen-lib 9.1.0", +] + +[[package]] +name = "vergen-gitcl" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9dfc1de6eb2e08a4ddf152f1b179529638bedc0ea95e6d667c014506377aefe" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time", + "vergen", + "vergen-lib 0.1.6", +] + +[[package]] +name = "vergen-lib" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + +[[package]] +name = "vergen-lib" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + [[package]] name = "version_check" version = "0.9.5" @@ -3378,6 +5192,19 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.221.3" @@ -3506,7 +5333,7 @@ dependencies = [ "rustix 0.38.44", "serde", "serde_derive", - "sha2", + "sha2 0.10.9", "toml", "windows-sys 0.59.0", "zstd", @@ -3720,6 +5547,31 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -3769,12 +5621,89 @@ dependencies = [ "wasmtime-environ", ] +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "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", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" 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", + "windows-link", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -3804,6 +5733,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -3840,6 +5778,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3873,6 +5826,21 @@ 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.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3885,6 +5853,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3897,6 +5871,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3921,6 +5901,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3933,6 +5919,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3945,6 +5937,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3957,6 +5955,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3978,6 +5982,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -4084,12 +6098,61 @@ dependencies = [ "wasmparser 0.244.0", ] +[[package]] +name = "wmi" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49d9da833ef7c4419d8c3a18f0f7a8eca8ccc85f7ab8f359281c24100251211" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.18", + "windows", + "windows-core", +] + [[package]] name = "writeable" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.18", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "yoke" version = "0.8.1" @@ -4113,6 +6176,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "z32" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" + [[package]] name = "zerocopy" version = "0.8.39" @@ -4159,6 +6228,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" diff --git a/crates/bin-runner/build.rs b/crates/bin-runner/build.rs new file mode 100644 index 0000000..c1c291d --- /dev/null +++ b/crates/bin-runner/build.rs @@ -0,0 +1,28 @@ +use std::path::Path; +use std::process::Command; + +fn main() { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let guests_dir = Path::new(manifest_dir).join("tests/guests"); + + for guest in &["echo", "double", "silent"] { + let guest_dir = guests_dir.join(guest); + + println!( + "cargo:rerun-if-changed={}", + guest_dir.join("src/lib.rs").display() + ); + println!( + "cargo:rerun-if-changed={}", + guest_dir.join("Cargo.toml").display() + ); + + let status = Command::new("cargo") + .args(["build", "--target", "wasm32-unknown-unknown", "--release"]) + .current_dir(&guest_dir) + .status() + .unwrap_or_else(|e| panic!("failed to run cargo build for {guest} guest: {e}")); + + assert!(status.success(), "failed to build {guest} guest"); + } +} diff --git a/crates/bin-runner/tests/wasm_actor.rs b/crates/bin-runner/tests/wasm_actor.rs index 717df9e..341a2d8 100644 --- a/crates/bin-runner/tests/wasm_actor.rs +++ b/crates/bin-runner/tests/wasm_actor.rs @@ -1,7 +1,7 @@ use swactor::actor::{ActorAddress, ActorInterface}; use swactor::runtime::{Ctx, Runtime, RuntimeConfig}; use swactor_bin_runner::{ByteMessage, SharedEngine, WasmActor, WasmActorBuilder, WasmActorError}; -use swactor_std::CtxWatching; +use swactor_std::{CtxWatching, StdExtension}; use proptest::prelude::*; @@ -719,7 +719,8 @@ impl ActorInterface for DeathCounter { #[test] fn watch_notification() { let engine = SharedEngine::new().unwrap(); - let rt = Runtime::new(RuntimeConfig::default()); + let rt = Runtime::new(RuntimeConfig::default()) + .with_extension(std::sync::Arc::new(StdExtension::new())); let count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); let target = WasmActorBuilder::new(engine, guest_wasm("silent")).build().unwrap(); diff --git a/crates/distribution/Cargo.toml b/crates/distribution/Cargo.toml index 1a560f0..2d7e1fd 100644 --- a/crates/distribution/Cargo.toml +++ b/crates/distribution/Cargo.toml @@ -3,12 +3,19 @@ name = "distribution" version = "0.1.0" edition = "2024" +[features] +default = ["tcp"] +tcp = [] +iroh = ["dep:iroh", "dep:tokio"] + [dependencies] swactor = { path = "../..", features = ["serde", "transport"] } ed25519-dalek = { version = "2", features = ["rand_core"] } rand_core = { version = "0.6", features = ["getrandom"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +iroh = { version = "0.96", optional = true } +tokio = { version = "1", features = ["rt-multi-thread"], optional = true } [dev-dependencies] serde_json = "1" diff --git a/crates/distribution/src/driver.rs b/crates/distribution/src/driver.rs index 2c82c0d..3be39ef 100644 --- a/crates/distribution/src/driver.rs +++ b/crates/distribution/src/driver.rs @@ -3,9 +3,15 @@ //! Translates outgoing `NodeAction`s into wire messages sent via `TcpTransport`, //! and dispatches incoming wire messages to the appropriate `DistributedNode` //! handler methods. +//! +//! The driver maintains a `PeerAddressBook` mapping `NodeId → SocketAddr`. +//! Address hints travel in the TCP wire frame (not in protocol messages), +//! keeping the protocol layer transport-agnostic. +use std::collections::HashMap; use std::net::{SocketAddr, TcpStream}; +use serde::{Deserialize, Serialize}; use swactor::actor::ActorAddress; use swactor::transport::{NetworkMessage, WireEnvelope}; @@ -21,25 +27,71 @@ use crate::types::NodeId; /// field is unused but required by the wire format. const SWIM_DEST: ActorAddress = ActorAddress([0u8; 32]); +// ─── Address Hints ────────────────────────────────────────────────────────── + +/// An address hint bundled in the TCP wire frame. +/// +/// Each outgoing TCP message includes the sender's own (NodeId, SocketAddr) +/// as a hint. JoinResponse messages include all known member addresses. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddressHint { + pub node_id: NodeId, + pub addr: SocketAddr, +} + +// ─── Peer Address Book ────────────────────────────────────────────────────── + +/// Maps NodeId → SocketAddr. Maintained by the TCP driver layer. +pub struct PeerAddressBook(HashMap); + +impl PeerAddressBook { + pub fn new() -> Self { + Self(HashMap::new()) + } + + /// Learn a node's address from a hint. + pub fn learn(&mut self, node_id: NodeId, addr: SocketAddr) { + self.0.insert(node_id, addr); + } + + /// Resolve a node's address. + pub fn resolve(&self, node_id: &NodeId) -> Option { + self.0.get(node_id).copied() + } + + /// Learn multiple hints at once. + pub fn bulk_learn(&mut self, hints: &[AddressHint]) { + for hint in hints { + self.learn(hint.node_id, hint.addr); + } + } +} + +// ─── NodeDriver ───────────────────────────────────────────────────────────── + /// Network driver that owns a `DistributedNode` and performs real TCP I/O. pub struct NodeDriver { node: DistributedNode, transport: TcpTransport, acceptor: TcpAcceptor, streams: Vec, + address_book: PeerAddressBook, + listen_addr: SocketAddr, } impl NodeDriver { - /// Create a new driver. Binds a TCP listener on the node's `listen_addr`. - pub fn new(config: DistributedNodeConfig) -> Result { - let listen_addr = config.listen_addr; + /// Create a new driver. Binds a TCP listener on `listen_addr`. + pub fn new(listen_addr: SocketAddr, config: DistributedNodeConfig) -> Result { let acceptor = TcpAcceptor::bind(listen_addr)?; + let actual_addr = acceptor.local_addr(); let node = DistributedNode::new(config); Ok(Self { node, transport: TcpTransport::pool(), acceptor, streams: Vec::new(), + address_book: PeerAddressBook::new(), + listen_addr: actual_addr, }) } @@ -50,7 +102,7 @@ impl NodeDriver { /// The address this driver is listening on. pub fn listen_addr(&self) -> SocketAddr { - self.acceptor.local_addr() + self.listen_addr } /// Access the underlying node (read-only). @@ -63,17 +115,55 @@ impl NodeDriver { &mut self.node } - /// Capture a snapshot of the node's state. + /// Capture a snapshot of the node's state, enriched with addresses + /// from the driver's address book. pub fn snapshot(&self) -> DistributionNodeSnapshot { - self.node.snapshot() + let mut snap = self.node.snapshot(); + snap.listen_addr = Some(self.listen_addr.to_string()); + + // Enrich member addresses from address book + for member in &mut snap.members { + if let Some(node_id) = parse_node_id_hex(&member.node_id) { + if let Some(addr) = self.address_book.resolve(&node_id) { + member.addr = Some(addr.to_string()); + } + } + } + + // Enrich routing neighbor addresses from address book + for neighbor in &mut snap.routing_neighbors { + if let Some(node_id) = parse_node_id_hex(&neighbor.node_id) { + if let Some(addr) = self.address_book.resolve(&node_id) { + neighbor.addr = Some(addr.to_string()); + } + } + } + + snap } /// Join a cluster by contacting seed nodes. /// - /// Sends `JoinRequest` messages to each seed over TCP. + /// Sends `JoinRequest` messages directly to each seed over TCP. + /// On receiving a `JoinResponse`, extracts address hints and passes + /// the member records to the protocol layer. pub fn join(&mut self, seeds: &[SocketAddr]) { - let actions = self.node.join(seeds); - self.send_actions(&actions); + for seed_addr in seeds { + if let Err(e) = self.send_join_request(*seed_addr) { + eprintln!("driver: join send error to {seed_addr}: {e}"); + } + } + } + + fn send_join_request(&mut self, seed_addr: SocketAddr) -> Result<(), swactor::Error> { + let msg = JoinRequest { + from: self.node.node_id(), + }; + let hints = vec![AddressHint { + node_id: self.node.node_id(), + addr: self.listen_addr, + }]; + self.send_wire_with_hints::(&msg, seed_addr, &hints) } /// Advance the node by one tick. @@ -91,7 +181,12 @@ impl NodeDriver { /// each to the appropriate handler, and sends any response actions. pub fn recv(&mut self) { let envelopes = self.acceptor.try_recv(&mut self.streams); - for (envelope, _peer_addr) in envelopes { + for (envelope, _peer_addr, hints_bytes) in envelopes { + if !hints_bytes.is_empty() { + if let Ok(hints) = serde_json::from_slice::>(&hints_bytes) { + self.learn_hints(&hints); + } + } let response_actions = self.dispatch_incoming(envelope); self.send_actions(&response_actions); } @@ -108,69 +203,83 @@ impl NodeDriver { } fn send_action(&mut self, action: &NodeAction) -> Result<(), swactor::Error> { + // Standard sender hint + let sender_hint = AddressHint { + node_id: self.node.node_id(), + addr: self.listen_addr, + }; + match action { NodeAction::SendPing { - to_addr, + to, sequence, piggyback, - .. } => { + let dest = self.resolve_addr(to)?; let msg = Ping { from: self.node.node_id(), - from_addr: self.node.listen_addr(), sequence: *sequence, piggyback: piggyback.clone(), }; - self.send_wire::(&msg, *to_addr) + self.send_wire_with_hints::(&msg, dest, &[sender_hint]) } NodeAction::SendAck { - to_addr, + to, sequence, piggyback, - .. } => { + let dest = self.resolve_addr(to)?; let msg = Ack { from: self.node.node_id(), sequence: *sequence, piggyback: piggyback.clone(), }; - self.send_wire::(&msg, *to_addr) + self.send_wire_with_hints::(&msg, dest, &[sender_hint]) } NodeAction::SendPingReq { - relay_addr, + relay, target, - target_addr, sequence, piggyback, - .. } => { + let dest = self.resolve_addr(relay)?; let msg = PingReq { from: self.node.node_id(), target: *target, - target_addr: *target_addr, sequence: *sequence, piggyback: piggyback.clone(), }; - self.send_wire::(&msg, *relay_addr) - } - - NodeAction::SendJoinRequest { to_addr } => { - let msg = JoinRequest { - from: self.node.node_id(), - addr: self.node.listen_addr(), - }; - self.send_wire::(&msg, *to_addr) + // Include target hint so the relay can forward + let mut hints = vec![sender_hint]; + if let Some(target_addr) = self.address_book.resolve(target) { + hints.push(AddressHint { + node_id: *target, + addr: target_addr, + }); + } + self.send_wire_with_hints::(&msg, dest, &hints) } NodeAction::SendJoinResponse { - to_addr, members, .. + to, members, } => { + let dest = self.resolve_addr(to)?; let msg = JoinResponse { members: members.clone(), }; - self.send_wire::(&msg, *to_addr) + // Include all known member addresses as hints + let mut hints = vec![sender_hint]; + for record in members { + if let Some(addr) = self.address_book.resolve(&record.node_id) { + hints.push(AddressHint { + node_id: record.node_id, + addr, + }); + } + } + self.send_wire_with_hints::(&msg, dest, &hints) } NodeAction::MembershipChanged { .. } => { @@ -180,19 +289,46 @@ impl NodeDriver { } } - fn send_wire( + fn resolve_addr(&self, node_id: &NodeId) -> Result { + self.address_book + .resolve(node_id) + .ok_or_else(|| swactor::Error::from(format!( + "no address known for node {:?}", + node_id + ))) + } + + fn send_wire_with_hints( &mut self, msg: &M, dest_addr: SocketAddr, + hints: &[AddressHint], ) -> Result<(), swactor::Error> { let payload = serde_json::to_vec(msg) .map_err(|e| swactor::Error::from(format!("encode {}: {e}", M::type_tag())))?; + let hints_bytes = serde_json::to_vec(hints).unwrap_or_default(); let envelope = WireEnvelope { dest: SWIM_DEST, type_tag: M::type_tag().to_string(), payload, }; - self.transport.send_to(dest_addr, envelope) + let buf = encode_wire_envelope_with_hints(&envelope, &hints_bytes); + let mut stream = self.transport_get_or_connect(dest_addr)?; + use std::io::Write; + match stream.write_all(&buf) { + Ok(()) => Ok(()), + Err(_) => { + self.transport.evict(dest_addr); + let mut stream = self.transport_get_or_connect(dest_addr)?; + stream + .write_all(&buf) + .map_err(|e| swactor::Error::from(format!("TCP send to {dest_addr}: {e}"))) + } + } + } + + fn transport_get_or_connect(&self, addr: SocketAddr) -> Result { + self.transport.get_or_connect(addr) } // ─── Incoming: TCP → handler ──────────────────────────────────────── @@ -200,12 +336,13 @@ impl NodeDriver { fn dispatch_incoming(&mut self, envelope: WireEnvelope) -> Vec { match envelope.type_tag.as_str() { "swactor_dist::Ping" => match decode::(&envelope.payload) { - Ok(msg) => self.node.handle_ping( - msg.from, - msg.from_addr, - msg.sequence, - &msg.piggyback, - ), + Ok(msg) => { + self.node.handle_ping( + msg.from, + msg.sequence, + &msg.piggyback, + ) + } Err(e) => { eprintln!("driver: decode Ping: {e}"); Vec::new() @@ -224,7 +361,6 @@ impl NodeDriver { Ok(msg) => self.node.handle_ping_req( msg.from, msg.target, - msg.target_addr, msg.sequence, &msg.piggyback, ), @@ -235,7 +371,7 @@ impl NodeDriver { }, "swactor_dist::JoinRequest" => match decode::(&envelope.payload) { - Ok(msg) => self.node.handle_join_request(msg.from, msg.addr), + Ok(msg) => self.node.handle_join_request(msg.from), Err(e) => { eprintln!("driver: decode JoinRequest: {e}"); Vec::new() @@ -256,8 +392,43 @@ impl NodeDriver { } } } + + /// Process address hints extracted from incoming frames. + pub fn learn_hints(&mut self, hints: &[AddressHint]) { + self.address_book.bulk_learn(hints); + } } fn decode(bytes: &[u8]) -> Result { serde_json::from_slice(bytes).map_err(|e| e.to_string()) } + +fn parse_node_id_hex(hex: &str) -> Option { + if hex.len() != 64 { + return None; + } + let mut bytes = [0u8; 32]; + for i in 0..32 { + bytes[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?; + } + Some(NodeId(bytes)) +} + +/// Encode a wire envelope with address hints appended after the payload. +/// +/// Extended frame format: +/// [4B frame_len][32B dest][4B tag_len][tag][4B hints_len][hints][payload] +fn encode_wire_envelope_with_hints(envelope: &WireEnvelope, hints_bytes: &[u8]) -> Vec { + let tag_bytes = envelope.type_tag.as_bytes(); + let frame_len: u32 = (32 + 4 + tag_bytes.len() + 4 + hints_bytes.len() + envelope.payload.len()) as u32; + + let mut buf = Vec::with_capacity(4 + frame_len as usize); + buf.extend_from_slice(&frame_len.to_be_bytes()); + buf.extend_from_slice(&envelope.dest.0); + buf.extend_from_slice(&(tag_bytes.len() as u32).to_be_bytes()); + buf.extend_from_slice(tag_bytes); + buf.extend_from_slice(&(hints_bytes.len() as u32).to_be_bytes()); + buf.extend_from_slice(hints_bytes); + buf.extend_from_slice(&envelope.payload); + buf +} diff --git a/crates/distribution/src/iroh_driver.rs b/crates/distribution/src/iroh_driver.rs new file mode 100644 index 0000000..a6cc41e --- /dev/null +++ b/crates/distribution/src/iroh_driver.rs @@ -0,0 +1,465 @@ +//! iroh-based P2P network driver for `DistributedNode`. +//! +//! Provides the same driver pattern as `NodeDriver` (TCP), but uses iroh's +//! QUIC-based peer-to-peer transport with built-in TLS, NAT hole-punching, +//! and relay server fallback. +//! +//! The driver owns a tokio runtime internally, exposing a synchronous API +//! (`tick()`, `recv()`, `join()`) to match the existing main loop pattern. + +use std::collections::HashMap; +use std::time::Duration; + +use iroh::endpoint::Connection; +use iroh::{Endpoint, PublicKey, RelayMode, SecretKey}; +use tokio::runtime::Runtime as TokioRuntime; + + +use crate::crypto::Keypair; +use crate::messages::*; +use crate::node::{DistributedNode, DistributedNodeConfig}; +use crate::snapshot::DistributionNodeSnapshot; +use crate::swim::node::NodeAction; +use crate::types::NodeId; + +/// ALPN protocol identifier for SWIM messages over iroh. +const ALPN: &[u8] = b"swactor/swim/1"; + +// ─── Config ───────────────────────────────────────────────────────────────── + +/// Configuration for the iroh-based driver. +pub struct IrohDriverConfig { + /// Secret key for the iroh endpoint. + /// If `None`, a fresh key is generated (node gets a random identity). + pub secret_key: Option, + /// Relay server configuration. + /// Defaults to `RelayMode::Default` (n0 production relays). + pub relay_mode: RelayMode, + /// Protocol-layer configuration. + pub node: DistributedNodeConfig, +} + +// ─── Driver ───────────────────────────────────────────────────────────────── + +/// iroh P2P network driver. +/// +/// Bridges the synchronous `DistributedNode` state machine with iroh's +/// async QUIC transport. Owns a tokio runtime internally. +pub struct IrohDriver { + node: DistributedNode, + endpoint: Endpoint, + rt: TokioRuntime, + connections: HashMap, +} + +impl IrohDriver { + /// Create a new iroh driver. + /// + /// Builds a tokio runtime, creates an iroh `Endpoint`, and initializes + /// the protocol-layer `DistributedNode`. + pub fn new(config: IrohDriverConfig) -> Result> { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + + let endpoint = rt.block_on(async { + let mut builder = Endpoint::builder() + .alpns(vec![ALPN.to_vec()]) + .relay_mode(config.relay_mode); + + if let Some(key) = config.secret_key { + builder = builder.secret_key(key); + } + + builder.bind().await + })?; + + // Create a DistributedNode whose identity matches the iroh endpoint. + // Both use ed25519-dalek, so we can reconstruct our Keypair from iroh's secret key. + let iroh_secret = endpoint.secret_key().to_bytes(); + let keypair = Keypair::from_bytes(&iroh_secret); + let node = DistributedNode::with_keypair(keypair, config.node); + + Ok(Self { + node, + endpoint, + rt, + connections: HashMap::new(), + }) + } + + /// The node's identity. + pub fn node_id(&self) -> NodeId { + self.node.node_id() + } + + /// Access the underlying node (read-only). + pub fn node(&self) -> &DistributedNode { + &self.node + } + + /// Access the underlying node (mutable). + pub fn node_mut(&mut self) -> &mut DistributedNode { + &mut self.node + } + + /// Capture a snapshot enriched with iroh endpoint info. + pub fn snapshot(&self) -> DistributionNodeSnapshot { + let mut snap = self.node.snapshot(); + // Use iroh endpoint address as the "listen address" + let addr_info = self.rt.block_on(async { + format!("{}", self.endpoint.id()) + }); + snap.listen_addr = Some(addr_info); + snap + } + + /// Join a cluster by connecting to seed nodes via iroh. + /// + /// Each seed is identified by its iroh `PublicKey` (= our `NodeId`). + pub fn join(&mut self, seeds: &[PublicKey]) { + for seed_key in seeds { + let seed_node_id = NodeId(*seed_key.as_bytes()); + if let Err(e) = self.send_join_request(*seed_key, seed_node_id) { + eprintln!("iroh driver: join error to {seed_key}: {e}"); + } + } + } + + fn send_join_request( + &mut self, + seed_key: PublicKey, + seed_node_id: NodeId, + ) -> Result<(), Box> { + let msg = JoinRequest { + from: self.node.node_id(), + }; + let payload = serde_json::to_vec(&msg)?; + let tag = ::type_tag(); + + let endpoint = self.endpoint.clone(); + let conn = self.rt.block_on(async { + let conn = endpoint.connect(seed_key, ALPN).await?; + let mut send = conn.open_uni().await?; + write_message(&mut send, tag.as_bytes(), &payload).await?; + send.finish()?; + Ok::<_, Box>(conn) + })?; + + self.connections.insert(seed_node_id, conn); + Ok(()) + } + + /// Advance the node by one tick. + pub fn tick(&mut self) { + let actions = self.node.tick(); + self.send_actions(&actions); + } + + /// Process incoming iroh connections and messages (non-blocking). + pub fn recv(&mut self) { + let (incoming, new_conns) = self.rt.block_on(async { + self.receive_pending().await + }); + // Cache connections accepted from remote peers + for (node_id, conn) in new_conns { + self.connections.entry(node_id).or_insert(conn); + } + for (tag, payload, from_key) in incoming { + let from = NodeId(*from_key.as_bytes()); + let response_actions = self.dispatch_incoming(&tag, &payload, from); + self.send_actions(&response_actions); + } + } + + // ─── Outgoing: NodeAction → iroh ───────────────────────────────── + + fn send_actions(&mut self, actions: &[NodeAction]) { + for action in actions { + if let Err(e) = self.send_action(action) { + eprintln!("iroh driver: send error: {e}"); + } + } + } + + fn send_action(&mut self, action: &NodeAction) -> Result<(), Box> { + match action { + NodeAction::SendPing { + to, + sequence, + piggyback, + } => { + let msg = Ping { + from: self.node.node_id(), + sequence: *sequence, + piggyback: piggyback.clone(), + }; + self.send_message(to, &msg) + } + + NodeAction::SendAck { + to, + sequence, + piggyback, + } => { + let msg = Ack { + from: self.node.node_id(), + sequence: *sequence, + piggyback: piggyback.clone(), + }; + self.send_message(to, &msg) + } + + NodeAction::SendPingReq { + relay, + target, + sequence, + piggyback, + } => { + let msg = PingReq { + from: self.node.node_id(), + target: *target, + sequence: *sequence, + piggyback: piggyback.clone(), + }; + self.send_message(relay, &msg) + } + + NodeAction::SendJoinResponse { to, members } => { + let msg = JoinResponse { + members: members.clone(), + }; + self.send_message(to, &msg) + } + + NodeAction::MembershipChanged { .. } => Ok(()), + } + } + + fn send_message( + &mut self, + to: &NodeId, + msg: &M, + ) -> Result<(), Box> { + let tag = M::type_tag(); + let payload = serde_json::to_vec(msg)?; + let target_key = PublicKey::from_bytes(&to.0)?; + + let conn = self.get_or_connect(*to, target_key)?; + + let result = self.rt.block_on(async { + let mut send = conn.open_uni().await?; + write_message(&mut send, tag.as_bytes(), &payload).await?; + send.finish()?; + Ok::<_, Box>(()) + }); + + if let Err(_) = result { + // Connection may be stale, remove and retry once + self.connections.remove(to); + let conn = self.get_or_connect(*to, target_key)?; + self.rt.block_on(async { + let mut send = conn.open_uni().await?; + write_message(&mut send, tag.as_bytes(), &payload).await?; + send.finish()?; + Ok::<_, Box>(()) + })?; + } + + Ok(()) + } + + fn get_or_connect( + &mut self, + node_id: NodeId, + key: PublicKey, + ) -> Result> { + // Check for cached connection that's still open + if let Some(conn) = self.connections.get(&node_id) { + if conn.close_reason().is_none() { + return Ok(conn.clone()); + } + // Connection closed, remove it + self.connections.remove(&node_id); + } + + let endpoint = self.endpoint.clone(); + let conn = self.rt.block_on(async { + endpoint.connect(key, ALPN).await + })?; + + self.connections.insert(node_id, conn.clone()); + Ok(conn) + } + + // ─── Incoming: iroh → handler ──────────────────────────────────── + + async fn receive_pending(&self) -> (Vec<(String, Vec, PublicKey)>, Vec<(NodeId, Connection)>) { + let mut messages = Vec::new(); + let mut new_connections = Vec::new(); + + // Poll for incoming connections with a short timeout + loop { + let accept_fut = self.endpoint.accept(); + let result = tokio::time::timeout(Duration::from_millis(1), accept_fut).await; + + match result { + Ok(Some(incoming)) => { + if let Ok(conn) = incoming.await { + let remote_id = conn.remote_id(); + self.read_streams(&conn, remote_id, &mut messages).await; + let node_id = NodeId(*remote_id.as_bytes()); + new_connections.push((node_id, conn)); + } + } + _ => break, + } + } + + // Also read from existing cached connections + let conn_snapshot: Vec<(NodeId, Connection)> = self + .connections + .iter() + .map(|(id, c)| (*id, c.clone())) + .collect(); + + for (node_id, conn) in conn_snapshot { + let remote_id = PublicKey::from_bytes(&node_id.0).unwrap(); + self.read_streams(&conn, remote_id, &mut messages).await; + } + + (messages, new_connections) + } + + async fn read_streams( + &self, + conn: &Connection, + remote_id: PublicKey, + messages: &mut Vec<(String, Vec, PublicKey)>, + ) { + loop { + match tokio::time::timeout(Duration::from_millis(1), conn.accept_uni()).await { + Ok(Ok(mut recv)) => { + match read_message(&mut recv).await { + Ok((tag, payload)) => { + messages.push((tag, payload, remote_id)); + } + Err(e) => { + eprintln!("iroh driver: read error: {e}"); + break; + } + } + } + _ => break, + } + } + } + + fn dispatch_incoming( + &mut self, + tag: &str, + payload: &[u8], + _from: NodeId, + ) -> Vec { + match tag { + "swactor_dist::Ping" => match serde_json::from_slice::(payload) { + Ok(msg) => self.node.handle_ping(msg.from, msg.sequence, &msg.piggyback), + Err(e) => { + eprintln!("iroh driver: decode Ping: {e}"); + Vec::new() + } + }, + + "swactor_dist::Ack" => match serde_json::from_slice::(payload) { + Ok(msg) => self.node.handle_ack(msg.from, msg.sequence, &msg.piggyback), + Err(e) => { + eprintln!("iroh driver: decode Ack: {e}"); + Vec::new() + } + }, + + "swactor_dist::PingReq" => match serde_json::from_slice::(payload) { + Ok(msg) => { + self.node + .handle_ping_req(msg.from, msg.target, msg.sequence, &msg.piggyback) + } + Err(e) => { + eprintln!("iroh driver: decode PingReq: {e}"); + Vec::new() + } + }, + + "swactor_dist::JoinRequest" => { + match serde_json::from_slice::(payload) { + Ok(msg) => self.node.handle_join_request(msg.from), + Err(e) => { + eprintln!("iroh driver: decode JoinRequest: {e}"); + Vec::new() + } + } + } + + "swactor_dist::JoinResponse" => { + match serde_json::from_slice::(payload) { + Ok(msg) => self.node.handle_join_response(msg.members), + Err(e) => { + eprintln!("iroh driver: decode JoinResponse: {e}"); + Vec::new() + } + } + } + + other => { + eprintln!("iroh driver: unknown message type: {other}"); + Vec::new() + } + } + } + + /// Shut down the iroh endpoint. + pub fn shutdown(&self) { + self.rt.block_on(async { + self.endpoint.close().await; + }); + } +} + +// ─── Wire Framing Over QUIC Streams ───────────────────────────────────────── + +/// Write a tagged message to a QUIC send stream. +/// +/// Frame format: `[4B tag_len][tag_bytes][payload_bytes]` +async fn write_message( + send: &mut iroh::endpoint::SendStream, + tag: &[u8], + payload: &[u8], +) -> Result<(), Box> { + let tag_len = (tag.len() as u32).to_be_bytes(); + send.write_all(&tag_len).await?; + send.write_all(tag).await?; + send.write_all(payload).await?; + Ok(()) +} + +/// Read a tagged message from a QUIC recv stream. +/// +/// Returns `(type_tag, payload)`. +async fn read_message( + recv: &mut iroh::endpoint::RecvStream, +) -> Result<(String, Vec), Box> { + let mut tag_len_buf = [0u8; 4]; + recv.read_exact(&mut tag_len_buf).await?; + let tag_len = u32::from_be_bytes(tag_len_buf) as usize; + + if tag_len > 1024 { + return Err("tag too large".into()); + } + + let mut tag_buf = vec![0u8; tag_len]; + recv.read_exact(&mut tag_buf).await?; + let tag = String::from_utf8(tag_buf)?; + + let payload = recv.read_to_end(64 * 1024).await?; + + Ok((tag, payload)) +} diff --git a/crates/distribution/src/kademlia/lookup.rs b/crates/distribution/src/kademlia/lookup.rs index fe0061c..ac93965 100644 --- a/crates/distribution/src/kademlia/lookup.rs +++ b/crates/distribution/src/kademlia/lookup.rs @@ -10,7 +10,6 @@ //! translates into real network requests. use std::collections::{HashMap, HashSet}; -use std::net::SocketAddr; use crate::types::NodeId; use super::routing_table::{RoutingTable, K}; @@ -25,9 +24,9 @@ const MAX_ROUNDS: usize = 20; #[derive(Debug, Clone)] pub enum LookupAction { /// Send a FIND_NODE query to this node. - Query { node_id: NodeId, addr: SocketAddr }, + Query { node_id: NodeId }, /// The lookup is complete — here are the k closest nodes found. - Done { closest: Vec<(NodeId, SocketAddr)> }, + Done { closest: Vec }, } /// State of a single iterative FIND_NODE lookup. @@ -36,7 +35,7 @@ pub struct NodeLookup { k: usize, alpha: usize, /// All nodes discovered during the lookup, with their distances. - known: HashMap, + known: HashMap, /// Nodes we've already queried. queried: HashSet, /// Nodes we've sent queries to but haven't received responses yet. @@ -63,7 +62,7 @@ impl NodeLookup { let mut known = HashMap::new(); for entry in &seeds { let dist = entry.node_id.xor_distance(&target); - known.insert(entry.node_id, (entry.addr, dist)); + known.insert(entry.node_id, dist); } let mut lookup = Self { @@ -85,7 +84,7 @@ impl NodeLookup { pub fn handle_response( &mut self, from: NodeId, - closer_nodes: Vec<(NodeId, SocketAddr)>, + closer_nodes: Vec, ) -> Vec { if self.done { return vec![self.done_action()]; @@ -94,14 +93,13 @@ impl NodeLookup { self.pending.remove(&from); // Incorporate newly discovered nodes - for (node_id, addr) in closer_nodes { + for node_id in closer_nodes { if node_id == self.target { // Skip the target itself (it's what we're looking for) continue; } self.known.entry(node_id).or_insert_with(|| { - let dist = node_id.xor_distance(&self.target); - (addr, dist) + node_id.xor_distance(&self.target) }); } @@ -140,10 +138,10 @@ impl NodeLookup { .known .iter() .filter(|(id, _)| !self.queried.contains(id)) - .map(|(id, (addr, dist))| (*id, *addr, *dist)) + .map(|(id, dist)| (*id, *dist)) .collect(); - candidates.sort_by(|a, b| a.2.cmp(&b.2)); + candidates.sort_by(|a, b| a.1.cmp(&b.1)); candidates.truncate(self.alpha); if candidates.is_empty() { @@ -157,7 +155,7 @@ impl NodeLookup { let all_k_queried = all_known_sorted .iter() .take(self.k) - .all(|(id, _)| self.queried.contains(id)); + .all(|id| self.queried.contains(id)); if all_k_queried && !all_known_sorted.is_empty() { self.done = true; @@ -165,24 +163,24 @@ impl NodeLookup { } let mut actions = Vec::new(); - for (node_id, addr, _) in candidates { + for (node_id, _) in candidates { self.queried.insert(node_id); self.pending.insert(node_id); - actions.push(LookupAction::Query { node_id, addr }); + actions.push(LookupAction::Query { node_id }); } actions } - fn k_closest(&self) -> Vec<(NodeId, SocketAddr)> { + fn k_closest(&self) -> Vec { let mut sorted: Vec<_> = self .known .iter() - .map(|(id, (addr, dist))| (*id, *addr, *dist)) + .map(|(id, dist)| (*id, *dist)) .collect(); - sorted.sort_by(|a, b| a.2.cmp(&b.2)); + sorted.sort_by(|a, b| a.1.cmp(&b.1)); sorted.truncate(self.k); - sorted.into_iter().map(|(id, addr, _)| (id, addr)).collect() + sorted.into_iter().map(|(id, _)| id).collect() } fn done_action(&self) -> LookupAction { diff --git a/crates/distribution/src/kademlia/routing_table.rs b/crates/distribution/src/kademlia/routing_table.rs index 59cc215..fb11ee3 100644 --- a/crates/distribution/src/kademlia/routing_table.rs +++ b/crates/distribution/src/kademlia/routing_table.rs @@ -6,7 +6,6 @@ //! replacement cache and only promote when an existing node is evicted. use std::collections::VecDeque; -use std::net::SocketAddr; use crate::types::NodeId; @@ -20,7 +19,6 @@ const NUM_BUCKETS: usize = 256; #[derive(Debug, Clone)] pub struct NodeEntry { pub node_id: NodeId, - pub addr: SocketAddr, } /// A single k-bucket with an LRU list and replacement cache. @@ -119,12 +117,12 @@ impl RoutingTable { } /// Insert or update a node in the routing table. - pub fn insert(&mut self, node_id: NodeId, addr: SocketAddr) -> bool { + pub fn insert(&mut self, node_id: NodeId) -> bool { if node_id == self.self_id { return false; } let idx = self.bucket_index(&node_id); - self.buckets[idx].insert(NodeEntry { node_id, addr }) + self.buckets[idx].insert(NodeEntry { node_id }) } /// Remove a node from the routing table. diff --git a/crates/distribution/src/lib.rs b/crates/distribution/src/lib.rs index 18f5a41..503bbf0 100644 --- a/crates/distribution/src/lib.rs +++ b/crates/distribution/src/lib.rs @@ -2,6 +2,7 @@ pub mod types; pub mod crypto; pub mod messages; pub mod codec; +#[cfg(feature = "tcp")] pub mod transport; pub mod swim; pub mod kademlia; @@ -9,4 +10,7 @@ pub mod cache; pub mod node; pub mod registry; pub mod snapshot; +#[cfg(feature = "tcp")] pub mod driver; +#[cfg(feature = "iroh")] +pub mod iroh_driver; diff --git a/crates/distribution/src/messages.rs b/crates/distribution/src/messages.rs index ff3b63a..8cba98e 100644 --- a/crates/distribution/src/messages.rs +++ b/crates/distribution/src/messages.rs @@ -1,7 +1,5 @@ //! Protocol messages for SWIM membership and Kademlia directory. -use std::net::SocketAddr; - use serde::{Deserialize, Serialize}; use swactor::actor::ActorAddress; use swactor::transport::NetworkMessage; @@ -17,7 +15,6 @@ use crate::types::{DirectoryEntry, MemberState, NodeId, NodeRecord}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Ping { pub from: NodeId, - pub from_addr: SocketAddr, pub sequence: u64, #[serde(default)] pub piggyback: Vec, @@ -53,7 +50,6 @@ impl NetworkMessage for Ack { pub struct PingReq { pub from: NodeId, pub target: NodeId, - pub target_addr: SocketAddr, pub sequence: u64, #[serde(default)] pub piggyback: Vec, @@ -69,7 +65,6 @@ impl NetworkMessage for PingReq { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JoinRequest { pub from: NodeId, - pub addr: SocketAddr, } impl NetworkMessage for JoinRequest { @@ -96,7 +91,6 @@ impl NetworkMessage for JoinResponse { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MembershipUpdate { pub node_id: NodeId, - pub addr: SocketAddr, pub state: MemberState, pub incarnation: u64, } @@ -119,7 +113,7 @@ impl NetworkMessage for FindNodeRequest { /// Kademlia FIND_NODE response — closest known nodes. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FindNodeResponse { - pub closest: Vec<(NodeId, SocketAddr)>, + pub closest: Vec, } impl NetworkMessage for FindNodeResponse { @@ -159,7 +153,7 @@ pub enum FindValueResponse { /// Found the actor — here's the directory entry. Found(DirectoryEntry), /// Don't have it — here are closer nodes to ask. - Closer(Vec<(NodeId, SocketAddr)>), + Closer(Vec), } impl NetworkMessage for FindValueResponse { diff --git a/crates/distribution/src/node.rs b/crates/distribution/src/node.rs index 3d7d87b..d7e4690 100644 --- a/crates/distribution/src/node.rs +++ b/crates/distribution/src/node.rs @@ -3,8 +3,6 @@ //! Composes SWIM membership, Kademlia routing, directory, cache, and //! transport into a single public API. -use std::net::SocketAddr; - use swactor::actor::ActorAddress; use crate::cache::LocationCache; @@ -21,8 +19,8 @@ use crate::swim::probe::SwimConfig; use crate::types::{MemberState, NodeId, NodeRecord}; /// Configuration for a distributed node. +#[derive(Clone)] pub struct DistributedNodeConfig { - pub listen_addr: SocketAddr, pub swim: SwimConfig, pub cache_capacity: usize, pub republish_interval: u64, @@ -32,7 +30,6 @@ pub struct DistributedNodeConfig { impl Default for DistributedNodeConfig { fn default() -> Self { Self { - listen_addr: "127.0.0.1:0".parse().unwrap(), swim: SwimConfig::default(), cache_capacity: 10_000, republish_interval: 1000, @@ -68,7 +65,7 @@ impl DistributedNode { pub fn with_keypair(keypair: Keypair, config: DistributedNodeConfig) -> Self { let node_id = keypair.node_id(); Self { - swim: SwimNode::new(node_id, config.listen_addr, config.swim), + swim: SwimNode::new(node_id, config.swim), routing_table: RoutingTable::new(node_id), directory: DirectoryShard::new(), cache: LocationCache::new(config.cache_capacity), @@ -86,21 +83,12 @@ impl DistributedNode { self.keypair.node_id() } - pub fn listen_addr(&self) -> SocketAddr { - self.swim.self_addr() - } - pub fn keypair(&self) -> &Keypair { &self.keypair } // ─── Cluster operations ───────────────────────────────────────────── - /// Join a cluster by contacting seed nodes. - pub fn join(&self, seeds: &[SocketAddr]) -> Vec { - self.swim.join(seeds) - } - /// Leave the cluster gracefully. pub fn leave(&mut self) -> Vec { self.swim.leave() @@ -156,15 +144,12 @@ impl DistributedNode { // ─── SWIM message handling (delegate to SwimNode) ─────────────────── - pub fn handle_ping(&mut self, from: NodeId, from_addr: SocketAddr, sequence: u64, piggyback: &[u8]) -> Vec { + pub fn handle_ping(&mut self, from: NodeId, sequence: u64, piggyback: &[u8]) -> Vec { let (membership_bytes, registry_entries) = unpack_combined_piggyback(piggyback); - let actions = self.swim.handle_ping(from, from_addr, sequence, &membership_bytes); - // Process membership BEFORE merging registry — otherwise a death - // notification in this same piggyback would immediately tombstone - // freshly received registry entries instead of pre-existing ones. + let actions = self.swim.handle_ping(from, sequence, &membership_bytes); self.process_membership_changes(&actions); self.merge_registry_entries(registry_entries); - self.maybe_update_routing_table(from, from_addr); + self.maybe_update_routing_table(from); self.inject_registry_piggyback(actions) } @@ -176,24 +161,24 @@ impl DistributedNode { self.inject_registry_piggyback(actions) } - pub fn handle_ping_req(&mut self, from: NodeId, target: NodeId, target_addr: SocketAddr, sequence: u64, piggyback: &[u8]) -> Vec { + pub fn handle_ping_req(&mut self, from: NodeId, target: NodeId, sequence: u64, piggyback: &[u8]) -> Vec { let (membership_bytes, registry_entries) = unpack_combined_piggyback(piggyback); - let actions = self.swim.handle_ping_req(from, target, target_addr, sequence, &membership_bytes); + let actions = self.swim.handle_ping_req(from, target, sequence, &membership_bytes); self.process_membership_changes(&actions); self.merge_registry_entries(registry_entries); self.inject_registry_piggyback(actions) } - pub fn handle_join_request(&mut self, from: NodeId, from_addr: SocketAddr) -> Vec { - let actions = self.swim.handle_join_request(from, from_addr); - self.maybe_update_routing_table(from, from_addr); + pub fn handle_join_request(&mut self, from: NodeId) -> Vec { + let actions = self.swim.handle_join_request(from); + self.maybe_update_routing_table(from); actions } pub fn handle_join_response(&mut self, members: Vec) -> Vec { for m in &members { if m.state != MemberState::Dead { - self.routing_table.insert(m.node_id, m.addr); + self.routing_table.insert(m.node_id); } } self.swim.handle_join_response(members) @@ -240,7 +225,7 @@ impl DistributedNode { } ResolveResult::NeedsLookup { - closest_nodes: closest.into_iter().map(|e| (e.node_id, e.addr)).collect(), + closest_nodes: closest.into_iter().map(|e| e.node_id).collect(), } } @@ -305,8 +290,8 @@ impl DistributedNode { // ─── Internal ─────────────────────────────────────────────────────── - fn maybe_update_routing_table(&mut self, node_id: NodeId, addr: SocketAddr) { - self.routing_table.insert(node_id, addr); + fn maybe_update_routing_table(&mut self, node_id: NodeId) { + self.routing_table.insert(node_id); } fn process_membership_changes(&mut self, actions: &[NodeAction]) { @@ -320,9 +305,7 @@ impl DistributedNode { fn handle_membership_change(&mut self, node_id: NodeId, state: MemberState) { match state { MemberState::Alive => { - if let Some(entry) = self.swim.members().get(&node_id) { - self.routing_table.insert(node_id, entry.addr); - } + self.routing_table.insert(node_id); // Re-disseminate registry entries so the recovering node // catches up on state accumulated during the partition. self.registry.re_disseminate_all(self.cluster_size()); @@ -348,20 +331,20 @@ impl DistributedNode { actions .into_iter() .map(|action| match action { - NodeAction::SendPing { to, to_addr, sequence, piggyback } => { + NodeAction::SendPing { to, sequence, piggyback } => { let registry_entries = self.registry.take_pending(8); let combined = pack_combined_piggyback(piggyback, registry_entries); - NodeAction::SendPing { to, to_addr, sequence, piggyback: combined } + NodeAction::SendPing { to, sequence, piggyback: combined } } - NodeAction::SendAck { to, to_addr, sequence, piggyback } => { + NodeAction::SendAck { to, sequence, piggyback } => { let registry_entries = self.registry.take_pending(8); let combined = pack_combined_piggyback(piggyback, registry_entries); - NodeAction::SendAck { to, to_addr, sequence, piggyback: combined } + NodeAction::SendAck { to, sequence, piggyback: combined } } - NodeAction::SendPingReq { relay, relay_addr, target, target_addr, sequence, piggyback } => { + NodeAction::SendPingReq { relay, target, sequence, piggyback } => { let registry_entries = self.registry.take_pending(8); let combined = pack_combined_piggyback(piggyback, registry_entries); - NodeAction::SendPingReq { relay, relay_addr, target, target_addr, sequence, piggyback: combined } + NodeAction::SendPingReq { relay, target, sequence, piggyback: combined } } other => other, }) @@ -382,7 +365,7 @@ pub enum ResolveResult { /// Found in cache or local directory. Cached(NodeId), /// Need to do a Kademlia FIND_VALUE — here are the closest known nodes. - NeedsLookup { closest_nodes: Vec<(NodeId, SocketAddr)> }, + NeedsLookup { closest_nodes: Vec }, /// No nodes known at all. NotFound, } diff --git a/crates/distribution/src/registry.rs b/crates/distribution/src/registry.rs index 72f3690..e132709 100644 --- a/crates/distribution/src/registry.rs +++ b/crates/distribution/src/registry.rs @@ -14,6 +14,7 @@ use crate::types::NodeId; // ─── Configuration ────────────────────────────────────────────────────────── /// Configuration for the cluster registry. +#[derive(Clone)] pub struct RegistryConfig { /// Maximum number of events to buffer before dropping old ones. pub max_events: usize, diff --git a/crates/distribution/src/snapshot.rs b/crates/distribution/src/snapshot.rs index 32a6d1d..a0a568e 100644 --- a/crates/distribution/src/snapshot.rs +++ b/crates/distribution/src/snapshot.rs @@ -3,8 +3,6 @@ //! Used by the runtime-dashboard to display distribution monitoring data //! for a single node without reaching out to other nodes. -use std::net::SocketAddr; - use serde::{Deserialize, Serialize}; use crate::node::DistributedNode; @@ -14,7 +12,7 @@ use crate::types::{MemberState, NodeId}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MemberInfo { pub node_id: String, - pub addr: String, + pub addr: Option, pub state: String, pub incarnation: u64, } @@ -23,7 +21,7 @@ pub struct MemberInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NeighborInfo { pub node_id: String, - pub addr: String, + pub addr: Option, } /// Snapshot of a single LRU cache entry. @@ -47,8 +45,8 @@ pub struct RegistryEntryInfo { pub struct DistributionNodeSnapshot { /// This node's ID (hex-encoded). pub node_id: String, - /// This node's listen address. - pub listen_addr: String, + /// This node's listen address (filled by driver, None for protocol-only snapshots). + pub listen_addr: Option, // ─── SWIM membership ───────────────────────────────────────────── /// All known members with their state. @@ -97,10 +95,6 @@ fn node_id_hex(id: &NodeId) -> String { id.0.iter().map(|b| format!("{:02x}", b)).collect() } -fn addr_str(addr: &SocketAddr) -> String { - addr.to_string() -} - fn state_str(state: MemberState) -> String { match state { MemberState::Alive => "alive".into(), @@ -111,13 +105,16 @@ fn state_str(state: MemberState) -> String { impl DistributedNode { /// Capture a serializable snapshot of this node's current state. + /// + /// Address fields are left as `None` — the driver layer enriches them + /// from its own address book. pub fn snapshot(&self) -> DistributionNodeSnapshot { let all_members = self.all_members(); let members: Vec = all_members .iter() .map(|m| MemberInfo { node_id: node_id_hex(&m.node_id), - addr: addr_str(&m.addr), + addr: None, state: state_str(m.state), incarnation: m.incarnation, }) @@ -133,7 +130,7 @@ impl DistributedNode { .iter() .map(|n| NeighborInfo { node_id: node_id_hex(&n.node_id), - addr: addr_str(&n.addr), + addr: None, }) .collect(); @@ -166,7 +163,7 @@ impl DistributedNode { DistributionNodeSnapshot { node_id: node_id_hex(&self.node_id()), - listen_addr: addr_str(&self.listen_addr()), + listen_addr: None, members, alive_count, suspect_count, diff --git a/crates/distribution/src/swim/dissemination.rs b/crates/distribution/src/swim/dissemination.rs index 66eeeb9..d4aaf5e 100644 --- a/crates/distribution/src/swim/dissemination.rs +++ b/crates/distribution/src/swim/dissemination.rs @@ -6,8 +6,6 @@ //! //! Priority ordering: Dead > Suspect > Alive (most urgent first). -use std::net::SocketAddr; - use crate::messages::MembershipUpdate; use crate::types::{MemberState, NodeId}; @@ -121,13 +119,11 @@ impl DisseminationQueue { /// Convenience: create a `MembershipUpdate` from components. pub fn membership_update( node_id: NodeId, - addr: SocketAddr, state: MemberState, incarnation: u64, ) -> MembershipUpdate { MembershipUpdate { node_id, - addr, state, incarnation, } diff --git a/crates/distribution/src/swim/member_list.rs b/crates/distribution/src/swim/member_list.rs index 201b1c5..dfaef15 100644 --- a/crates/distribution/src/swim/member_list.rs +++ b/crates/distribution/src/swim/member_list.rs @@ -6,7 +6,6 @@ //! 2. Same incarnation: higher-priority state wins (Dead > Suspect > Alive). use std::collections::HashMap; -use std::net::SocketAddr; use crate::types::{MemberState, NodeId, NodeRecord}; @@ -14,7 +13,6 @@ use crate::types::{MemberState, NodeId, NodeRecord}; #[derive(Debug, Clone)] pub struct MemberEntry { pub node_id: NodeId, - pub addr: SocketAddr, pub state: MemberState, pub incarnation: u64, } @@ -23,7 +21,6 @@ impl MemberEntry { pub fn to_record(&self) -> NodeRecord { NodeRecord { node_id: self.node_id, - addr: self.addr, state: self.state, incarnation: self.incarnation, } @@ -108,7 +105,7 @@ impl MemberList { /// - Higher incarnation always wins. /// - Same incarnation: higher-priority state wins. /// - Lower incarnation is ignored. - pub fn apply(&mut self, node_id: NodeId, addr: SocketAddr, state: MemberState, incarnation: u64) -> bool { + pub fn apply(&mut self, node_id: NodeId, state: MemberState, incarnation: u64) -> bool { // Don't store entries about ourselves if node_id == self.self_id { return false; @@ -117,7 +114,6 @@ impl MemberList { match self.members.get_mut(&node_id) { Some(existing) => { if incarnation > existing.incarnation { - existing.addr = addr; existing.state = state; existing.incarnation = incarnation; true @@ -131,7 +127,6 @@ impl MemberList { None => { self.members.insert(node_id, MemberEntry { node_id, - addr, state, incarnation, }); @@ -151,10 +146,11 @@ impl MemberList { false } - /// Mark a node as dead. + /// Mark a node as dead. Only transitions from Suspect → Dead, + /// enforcing the SWIM lifecycle invariant (Alive → Suspect → Dead). pub fn declare_dead(&mut self, node_id: NodeId) -> bool { if let Some(entry) = self.members.get_mut(&node_id) { - if entry.state != MemberState::Dead { + if entry.state == MemberState::Suspect { entry.state = MemberState::Dead; return true; } diff --git a/crates/distribution/src/swim/node.rs b/crates/distribution/src/swim/node.rs index 6abffc6..6b5b25e 100644 --- a/crates/distribution/src/swim/node.rs +++ b/crates/distribution/src/swim/node.rs @@ -3,8 +3,6 @@ //! This is the top-level SWIM state machine that a `DistributedNode` will drive. //! It produces `SwimAction`s that the caller translates into real network I/O. -use std::net::SocketAddr; - use crate::messages::MembershipUpdate; use crate::types::{MemberState, NodeId, NodeRecord}; @@ -18,22 +16,18 @@ use super::probe::{SwimAction, SwimConfig, SwimEvent, SwimProbe}; #[derive(Debug, Clone)] pub enum NodeAction { /// Send a SWIM ping. - SendPing { to: NodeId, to_addr: SocketAddr, sequence: u64, piggyback: Vec }, + SendPing { to: NodeId, sequence: u64, piggyback: Vec }, /// Send an indirect ping request through a relay. SendPingReq { relay: NodeId, - relay_addr: SocketAddr, target: NodeId, - target_addr: SocketAddr, sequence: u64, piggyback: Vec, }, /// Send a SWIM ack. - SendAck { to: NodeId, to_addr: SocketAddr, sequence: u64, piggyback: Vec }, - /// Send a join request to a seed. - SendJoinRequest { to_addr: SocketAddr }, + SendAck { to: NodeId, sequence: u64, piggyback: Vec }, /// Send a join response with the current member list. - SendJoinResponse { to: NodeId, to_addr: SocketAddr, members: Vec }, + SendJoinResponse { to: NodeId, members: Vec }, /// Notification: a node state changed (for wiring into Kademlia). MembershipChanged { node_id: NodeId, state: MemberState, incarnation: u64 }, } @@ -44,18 +38,16 @@ pub struct SwimNode { members: MemberList, probe: SwimProbe, dissemination: DisseminationQueue, - self_addr: SocketAddr, /// Maximum piggybacked updates per message. max_piggyback: usize, } impl SwimNode { - pub fn new(self_id: NodeId, self_addr: SocketAddr, config: SwimConfig) -> Self { + pub fn new(self_id: NodeId, config: SwimConfig) -> Self { Self { members: MemberList::new(self_id), probe: SwimProbe::new(config), dissemination: DisseminationQueue::new(3), // Λ = 3 - self_addr, max_piggyback: 8, } } @@ -64,10 +56,6 @@ impl SwimNode { self.members.self_id() } - pub fn self_addr(&self) -> SocketAddr { - self.self_addr - } - pub fn members(&self) -> &MemberList { &self.members } @@ -84,17 +72,16 @@ impl SwimNode { } /// Handle a received ping. - pub fn handle_ping(&mut self, from: NodeId, from_addr: SocketAddr, sequence: u64, piggyback: &[u8]) -> Vec { + pub fn handle_ping(&mut self, from: NodeId, sequence: u64, piggyback: &[u8]) -> Vec { let mut actions = self.apply_piggyback(piggyback); // Ensure the sender is in our member list - self.members.apply(from, from_addr, MemberState::Alive, 0); + self.members.apply(from, MemberState::Alive, 0); // Reply with ack let pb = self.dissemination.pack_piggyback(self.max_piggyback); actions.push(NodeAction::SendAck { to: from, - to_addr: from_addr, sequence, piggyback: pb, }); @@ -117,7 +104,6 @@ impl SwimNode { &mut self, _from: NodeId, target: NodeId, - target_addr: SocketAddr, sequence: u64, piggyback: &[u8], ) -> Vec { @@ -127,7 +113,6 @@ impl SwimNode { let pb = self.dissemination.pack_piggyback(self.max_piggyback); actions.push(NodeAction::SendPing { to: target, - to_addr: target_addr, sequence, piggyback: pb, }); @@ -135,15 +120,15 @@ impl SwimNode { } /// Handle a join request from a new node. - pub fn handle_join_request(&mut self, from: NodeId, from_addr: SocketAddr) -> Vec { + pub fn handle_join_request(&mut self, from: NodeId) -> Vec { // Add the new node to our member list - let changed = self.members.apply(from, from_addr, MemberState::Alive, 0); + let changed = self.members.apply(from, MemberState::Alive, 0); let mut actions = Vec::new(); if changed { // Enqueue the join for dissemination self.dissemination.enqueue( - membership_update(from, from_addr, MemberState::Alive, 0), + membership_update(from, MemberState::Alive, 0), self.cluster_size(), ); actions.push(NodeAction::MembershipChanged { @@ -157,13 +142,11 @@ impl SwimNode { let mut members = self.members.snapshot(); members.push(NodeRecord { node_id: self.members.self_id(), - addr: self.self_addr, state: MemberState::Alive, incarnation: self.members.self_incarnation(), }); actions.push(NodeAction::SendJoinResponse { to: from, - to_addr: from_addr, members, }); @@ -176,7 +159,6 @@ impl SwimNode { for record in members { let changed = self.members.apply( record.node_id, - record.addr, record.state, record.incarnation, ); @@ -191,20 +173,11 @@ impl SwimNode { actions } - /// Initiate joining a cluster by contacting seed nodes. - pub fn join(&self, seeds: &[SocketAddr]) -> Vec { - seeds - .iter() - .map(|addr| NodeAction::SendJoinRequest { to_addr: *addr }) - .collect() - } - /// Announce ourselves as dead (graceful leave). pub fn leave(&mut self) -> Vec { self.dissemination.enqueue( membership_update( self.members.self_id(), - self.self_addr, MemberState::Dead, self.members.self_incarnation(), ), @@ -235,7 +208,6 @@ impl SwimNode { self.dissemination.enqueue( membership_update( self.members.self_id(), - self.self_addr, MemberState::Alive, new_inc, ), @@ -247,14 +219,13 @@ impl SwimNode { let changed = self.members.apply( update.node_id, - update.addr, update.state, update.incarnation, ); if changed { // Re-disseminate the update self.dissemination.enqueue( - membership_update(update.node_id, update.addr, update.state, update.incarnation), + membership_update(update.node_id, update.state, update.incarnation), self.cluster_size(), ); vec![NodeAction::MembershipChanged { @@ -271,7 +242,7 @@ impl SwimNode { let mut actions = Vec::new(); for pa in probe_actions { match pa { - SwimAction::SendPing { to, to_addr, sequence } => { + SwimAction::SendPing { to, sequence } => { // If the target is suspect or dead, re-enqueue its state // so it piggybacks on this message. This is the key mechanism // for partition-heal recovery: the target learns it was @@ -279,7 +250,7 @@ impl SwimNode { if let Some(entry) = self.members.get(&to) { if entry.state == MemberState::Dead || entry.state == MemberState::Suspect { self.dissemination.enqueue( - membership_update(to, to_addr, entry.state, entry.incarnation), + membership_update(to, entry.state, entry.incarnation), self.cluster_size(), ); } @@ -287,18 +258,15 @@ impl SwimNode { let pb = self.dissemination.pack_piggyback(self.max_piggyback); actions.push(NodeAction::SendPing { to, - to_addr, sequence, piggyback: pb, }); } - SwimAction::SendPingReq { relay, relay_addr, target, target_addr, sequence } => { + SwimAction::SendPingReq { relay, target, sequence } => { let pb = self.dissemination.pack_piggyback(self.max_piggyback); actions.push(NodeAction::SendPingReq { relay, - relay_addr, target, - target_addr, sequence, piggyback: pb, }); @@ -307,7 +275,7 @@ impl SwimNode { if self.members.suspect(node_id) { if let Some(entry) = self.members.get(&node_id) { self.dissemination.enqueue( - membership_update(node_id, entry.addr, MemberState::Suspect, entry.incarnation), + membership_update(node_id, MemberState::Suspect, entry.incarnation), self.cluster_size(), ); } @@ -319,14 +287,10 @@ impl SwimNode { } } SwimAction::DeclareDead(node_id) => { - // Note: declare_dead() was already called by SwimProbe::check_suspicion_timeouts(), - // so we must NOT call it again (it would return false since state is already Dead). - // We just need to disseminate the update and emit the MembershipChanged action. if let Some(entry) = self.members.get(&node_id) { let inc = entry.incarnation; - let addr = entry.addr; self.dissemination.enqueue( - membership_update(node_id, addr, MemberState::Dead, inc), + membership_update(node_id, MemberState::Dead, inc), self.cluster_size(), ); actions.push(NodeAction::MembershipChanged { @@ -340,7 +304,6 @@ impl SwimNode { self.dissemination.enqueue( membership_update( self.members.self_id(), - self.self_addr, MemberState::Alive, new_incarnation, ), diff --git a/crates/distribution/src/swim/probe.rs b/crates/distribution/src/swim/probe.rs index 928b4da..a85deea 100644 --- a/crates/distribution/src/swim/probe.rs +++ b/crates/distribution/src/swim/probe.rs @@ -4,7 +4,6 @@ //! No I/O, no timers — the caller drives the clock. use std::collections::VecDeque; -use std::net::SocketAddr; use crate::types::{MemberState, NodeId}; @@ -62,13 +61,11 @@ pub enum SwimEvent { #[derive(Debug, Clone)] pub enum SwimAction { /// Send a direct ping to a node. - SendPing { to: NodeId, to_addr: SocketAddr, sequence: u64 }, + SendPing { to: NodeId, sequence: u64 }, /// Send an indirect ping request through a relay. SendPingReq { relay: NodeId, - relay_addr: SocketAddr, target: NodeId, - target_addr: SocketAddr, sequence: u64, }, /// A node is now suspected. @@ -88,7 +85,6 @@ enum ProbePhase { /// Direct ping sent, waiting for ack. WaitingDirectAck { target: NodeId, - target_addr: SocketAddr, sequence: u64, sent_at: u64, }, @@ -184,7 +180,7 @@ impl SwimProbe { } /// Pick the next probe target using round-robin over a shuffled order. - fn pick_probe_target(&mut self, members: &MemberList) -> Option<(NodeId, SocketAddr)> { + fn pick_probe_target(&mut self, members: &MemberList) -> Option { let alive = members.alive_members(); if alive.is_empty() { return None; @@ -205,11 +201,11 @@ impl SwimProbe { let target_id = self.probe_order[self.probe_index]; self.probe_index += 1; - members.get(&target_id).map(|e| (e.node_id, e.addr)) + Some(target_id) } /// Pick `k` random relay nodes (excluding `target`). - fn pick_relays(&self, members: &MemberList, target: NodeId) -> Vec<(NodeId, SocketAddr)> { + fn pick_relays(&self, members: &MemberList, target: NodeId) -> Vec { let alive: Vec<_> = members .alive_members() .into_iter() @@ -222,7 +218,7 @@ impl SwimProbe { let mut relays = Vec::with_capacity(k); for i in 0..k { let idx = (start + i) % alive.len(); - relays.push((alive[idx].node_id, alive[idx].addr)); + relays.push(alive[idx].node_id); } relays } @@ -237,7 +233,7 @@ impl SwimProbe { self.next_probe_tick = self.tick + self.config.probe_interval; - if let Some((target, target_addr)) = self.pick_probe_target(&mut MemberList::clone_shallow(members)) { + if let Some(target) = self.pick_probe_target(&mut MemberList::clone_shallow(members)) { // Record this probe target in history if self.recent_targets.len() >= PROBE_HISTORY_SIZE { self.recent_targets.pop_front(); @@ -247,12 +243,10 @@ impl SwimProbe { let seq = self.next_sequence(); actions.push(SwimAction::SendPing { to: target, - to_addr: target_addr, sequence: seq, }); self.phase = ProbePhase::WaitingDirectAck { target, - target_addr, sequence: seq, sent_at: self.tick, }; @@ -261,20 +255,17 @@ impl SwimProbe { fn check_probe_timeout(&mut self, members: &MemberList, actions: &mut Vec) { match &self.phase { - ProbePhase::WaitingDirectAck { target, target_addr, sequence, sent_at } => { + ProbePhase::WaitingDirectAck { target, sequence, sent_at } => { if self.tick - sent_at >= self.config.probe_timeout { let target = *target; - let target_addr = *target_addr; let sequence = *sequence; // Send indirect probes through relays let relays = self.pick_relays(members, target); - for (relay, relay_addr) in relays { + for relay in relays { actions.push(SwimAction::SendPingReq { relay, - relay_addr, target, - target_addr, sequence, }); } @@ -351,18 +342,32 @@ impl SwimProbe { .collect(); for node_id in expired { - if members.declare_dead(node_id) { + // Only declare dead if still suspect. A refutation (Alive with + // higher incarnation) clears the suspect state in MemberList; + // honour that by dropping the stale timer instead of killing the node. + let still_suspect = members + .get(&node_id) + .is_some_and(|e| e.state == MemberState::Suspect); + + if still_suspect && members.declare_dead(node_id) { actions.push(SwimAction::DeclareDead(node_id)); + // If we're currently probing the dead node, cancel immediately + // so we can probe live members on this same tick. + match &self.phase { + ProbePhase::WaitingDirectAck { target, .. } + | ProbePhase::WaitingIndirectAck { target, .. } + if *target == node_id => + { + self.phase = ProbePhase::Idle; + } + _ => {} + } } self.cancel_suspicion_timer(node_id); } } /// Periodically ping a dead node to detect partition heals. - /// - /// Runs independently of the normal probe cycle. The piggyback exchange - /// triggers the dead node's refutation mechanism (incarnation bump), - /// which propagates back and resurrects the node. fn maybe_reprobe_dead(&mut self, members: &MemberList, actions: &mut Vec) { if self.config.dead_reprobe_interval == 0 { return; @@ -385,7 +390,6 @@ impl SwimProbe { let seq = self.next_sequence(); actions.push(SwimAction::SendPing { to: target.node_id, - to_addr: target.addr, sequence: seq, }); } @@ -394,11 +398,11 @@ impl SwimProbe { // Helper: we need a read-only borrow of members in pick_probe_target // while also having &mut self. Use a shallow clone pattern. impl MemberList { - /// Cheap snapshot of just the IDs and addresses for probe target selection. + /// Cheap snapshot of just the IDs for probe target selection. fn clone_shallow(original: &MemberList) -> MemberList { let mut copy = MemberList::new(original.self_id()); for entry in original.all_members() { - copy.apply(entry.node_id, entry.addr, entry.state, entry.incarnation); + copy.apply(entry.node_id, entry.state, entry.incarnation); } copy } diff --git a/crates/distribution/src/transport.rs b/crates/distribution/src/transport.rs index c918139..6cdfdaa 100644 --- a/crates/distribution/src/transport.rs +++ b/crates/distribution/src/transport.rs @@ -5,6 +5,8 @@ //! [32 bytes: dest address] //! [4 bytes: type_tag len (BE u32)] //! [N bytes: type_tag UTF-8] +//! [4 bytes: hints len (BE u32)] +//! [M bytes: hints (JSON, may be empty)] //! [remaining: payload bytes] use std::collections::HashMap; @@ -47,7 +49,7 @@ impl TcpTransport { } } - fn get_or_connect(&self, addr: SocketAddr) -> Result { + pub fn get_or_connect(&self, addr: SocketAddr) -> Result { let mut pool = self.pool.lock().unwrap(); if let Some(stream) = pool.get(&addr) { match stream.try_clone() { @@ -66,6 +68,11 @@ impl TcpTransport { Ok(stream) } + /// Evict a pooled connection for an address. + pub fn evict(&self, addr: SocketAddr) { + self.pool.lock().unwrap().remove(&addr); + } + /// Send an envelope to a specific address. /// /// If the write fails (e.g. stale connection from a dead peer), evicts @@ -121,7 +128,8 @@ impl TcpAcceptor { /// Non-blocking: accept new connections, read complete envelopes from them. /// Returns all envelopes that could be read without blocking. - pub fn try_recv(&self, streams: &mut Vec) -> Vec<(WireEnvelope, SocketAddr)> { + /// Each entry contains (envelope, peer address, raw address hint bytes). + pub fn try_recv(&self, streams: &mut Vec) -> Vec<(WireEnvelope, SocketAddr, Vec)> { // Accept new connections loop { match self.listener.accept() { @@ -142,7 +150,7 @@ impl TcpAcceptor { let peer = stream.peer_addr().unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap()); loop { match read_wire_envelope(stream) { - Ok(env) => envelopes.push((env, peer)), + Ok((env, hints)) => envelopes.push((env, peer, hints)), Err(ReadError::WouldBlock) => break, Err(ReadError::Disconnected) => { dead.push(i); @@ -172,13 +180,14 @@ impl TcpAcceptor { /// Encode a WireEnvelope to bytes in the length-prefixed wire format. pub fn encode_wire_envelope(envelope: &WireEnvelope) -> Vec { let tag_bytes = envelope.type_tag.as_bytes(); - let frame_len: u32 = (32 + 4 + tag_bytes.len() + envelope.payload.len()) as u32; + let frame_len: u32 = (32 + 4 + tag_bytes.len() + 4 + envelope.payload.len()) as u32; let mut buf = Vec::with_capacity(4 + frame_len as usize); buf.extend_from_slice(&frame_len.to_be_bytes()); buf.extend_from_slice(&envelope.dest.0); buf.extend_from_slice(&(tag_bytes.len() as u32).to_be_bytes()); buf.extend_from_slice(tag_bytes); + buf.extend_from_slice(&0u32.to_be_bytes()); // hints_len = 0 buf.extend_from_slice(&envelope.payload); buf } @@ -200,8 +209,8 @@ impl From for ReadError { } } -/// Read one WireEnvelope from a TCP stream. -fn read_wire_envelope(stream: &mut TcpStream) -> Result { +/// Read one WireEnvelope and address hints from a TCP stream. +fn read_wire_envelope(stream: &mut TcpStream) -> Result<(WireEnvelope, Vec), ReadError> { let mut len_buf = [0u8; 4]; stream.read_exact(&mut len_buf)?; let frame_len = u32::from_be_bytes(len_buf) as usize; @@ -215,17 +224,23 @@ fn read_wire_envelope(stream: &mut TcpStream) -> Result let tag_len = u32::from_be_bytes(frame[32..36].try_into().unwrap()) as usize; let type_tag = String::from_utf8_lossy(&frame[36..36 + tag_len]).to_string(); - let payload = frame[36 + tag_len..].to_vec(); + let after_tag = 36 + tag_len; + let hints_len = u32::from_be_bytes(frame[after_tag..after_tag + 4].try_into().unwrap()) as usize; + let hints_bytes = frame[after_tag + 4..after_tag + 4 + hints_len].to_vec(); + let payload = frame[after_tag + 4 + hints_len..].to_vec(); - Ok(WireEnvelope { - dest: ActorAddress(dest), - type_tag, - payload, - }) + Ok(( + WireEnvelope { + dest: ActorAddress(dest), + type_tag, + payload, + }, + hints_bytes, + )) } -/// Read a single envelope from a blocking stream. Public for use in tests/examples. -pub fn read_envelope_blocking(stream: &mut TcpStream) -> Result { +/// Read a single envelope and hints from a blocking stream. Public for use in tests/examples. +pub fn read_envelope_blocking(stream: &mut TcpStream) -> Result<(WireEnvelope, Vec), Error> { // Temporarily set blocking mode stream .set_nonblocking(false) @@ -247,13 +262,19 @@ pub fn read_envelope_blocking(stream: &mut TcpStream) -> Result DistributedNodeConfig { + DistributedNodeConfig { + swim: SwimConfig { + probe_interval: 1, + probe_timeout: 3, + indirect_probes: 1, + suspicion_timeout: 5, + dead_reprobe_interval: 0, + }, + cache_capacity: 100, + republish_interval: 50, + registry: RegistryConfig::default(), + } +} + +/// Deliver actions to the appropriate target nodes, returning responses +/// tagged with the responder's index. Nodes whose index appears in +/// `excluded` silently drop messages (simulates death / network loss). +fn deliver_actions_tagged( + actions: &[NodeAction], + sender_id: NodeId, + ids: &[NodeId], + nodes: &mut [DistributedNode], + excluded: &[usize], +) -> Vec<(usize, Vec)> { + let mut tagged: Vec<(usize, Vec)> = Vec::new(); + + for action in actions { + match action { + NodeAction::SendPing { + to, + sequence, + piggyback, + .. + } => { + if let Some(idx) = ids.iter().position(|id| id == to) { + if !excluded.contains(&idx) { + let resp = nodes[idx].handle_ping(sender_id, *sequence, piggyback); + if !resp.is_empty() { + tagged.push((idx, resp)); + } + } + } + } + NodeAction::SendAck { + to, + sequence, + piggyback, + .. + } => { + if let Some(idx) = ids.iter().position(|id| id == to) { + if !excluded.contains(&idx) { + let resp = nodes[idx].handle_ack(sender_id, *sequence, piggyback); + if !resp.is_empty() { + tagged.push((idx, resp)); + } + } + } + } + NodeAction::SendJoinResponse { to, members, .. } => { + if let Some(idx) = ids.iter().position(|id| id == to) { + if !excluded.contains(&idx) { + let resp = nodes[idx].handle_join_response(members.clone()); + if !resp.is_empty() { + tagged.push((idx, resp)); + } + } + } + } + NodeAction::SendPingReq { + relay, + target, + sequence, + piggyback, + .. + } => { + if let Some(idx) = ids.iter().position(|id| id == relay) { + if !excluded.contains(&idx) { + let resp = + nodes[idx].handle_ping_req(sender_id, *target, *sequence, piggyback); + if !resp.is_empty() { + tagged.push((idx, resp)); + } + } + } + } + NodeAction::MembershipChanged { .. } => {} + } + } + + tagged +} + +/// An N-node test cluster with correct-by-construction message delivery. +pub struct TestCluster { + ids: Vec, + nodes: Vec, +} + +impl TestCluster { + /// Create an N-node cluster using `test_config()`. Nodes 1..N join via node 0. + pub fn new(n: usize) -> Self { + Self::with_config(n, test_config()) + } + + /// Create an N-node cluster with a custom config. Nodes 1..N join via node 0. + pub fn with_config(n: usize, config: DistributedNodeConfig) -> Self { + assert!(n >= 2, "TestCluster requires at least 2 nodes"); + + let mut nodes: Vec = + (0..n).map(|_| DistributedNode::new(config.clone())).collect(); + let ids: Vec = nodes.iter().map(|node| node.node_id()).collect(); + + // All nodes join through node 0 (seed). + for i in 1..n { + let actions = nodes[0].handle_join_request(ids[i]); + deliver_actions_tagged(&actions, ids[0], &ids, &mut nodes, &[]); + } + + Self { ids, nodes } + } + + /// Get the `NodeId` for the node at `idx`. + pub fn node_id(&self, idx: usize) -> NodeId { + self.ids[idx] + } + + /// Run one gossip round: tick all live nodes, deliver with tagged + /// responses, deliver responses back. Excluded indices are skipped. + fn gossip_round_excluding(&mut self, excluded: &[usize]) { + let n = self.nodes.len(); + + // 1. Tick all live nodes, collect actions. + let mut all_actions: Vec<(usize, Vec)> = Vec::new(); + for idx in 0..n { + if excluded.contains(&idx) { + continue; + } + let actions = self.nodes[idx].tick(); + if !actions.is_empty() { + all_actions.push((idx, actions)); + } + } + + // 2. Deliver each sender's actions → get tagged responses. + // 3. Deliver responses back using the responder's identity. + for (sender_idx, actions) in all_actions { + let tagged_responses = deliver_actions_tagged( + &actions, + self.ids[sender_idx], + &self.ids, + &mut self.nodes, + excluded, + ); + for (responder_idx, response_actions) in tagged_responses { + deliver_actions_tagged( + &response_actions, + self.ids[responder_idx], + &self.ids, + &mut self.nodes, + excluded, + ); + } + } + } + + /// Run `n` gossip rounds with all nodes participating. + pub fn gossip_rounds(&mut self, n: usize) { + for _ in 0..n { + self.gossip_round_excluding(&[]); + } + } + + /// Run `n` gossip rounds; dead nodes neither tick nor receive. + pub fn gossip_rounds_excluding(&mut self, dead: &[usize], n: usize) { + for _ in 0..n { + self.gossip_round_excluding(dead); + } + } +} + +impl Index for TestCluster { + type Output = DistributedNode; + fn index(&self, idx: usize) -> &Self::Output { + &self.nodes[idx] + } +} + +impl IndexMut for TestCluster { + fn index_mut(&mut self, idx: usize) -> &mut Self::Output { + &mut self.nodes[idx] + } +} diff --git a/crates/distribution/tests/iroh_driver.rs b/crates/distribution/tests/iroh_driver.rs new file mode 100644 index 0000000..82cf7c5 --- /dev/null +++ b/crates/distribution/tests/iroh_driver.rs @@ -0,0 +1,69 @@ +//! Integration tests for the iroh-based P2P driver. +//! +//! These tests verify that IrohDriver can: +//! - Create endpoints with matching identities +//! - Form clusters via join +//! - Detect membership changes through SWIM +//! +//! Requires the `iroh` feature. + +#![cfg(feature = "iroh")] + +use distribution::iroh_driver::{IrohDriver, IrohDriverConfig}; +use distribution::node::DistributedNodeConfig; +use distribution::registry::RegistryConfig; +use distribution::swim::probe::SwimConfig; +use iroh::RelayMode; + +fn test_config() -> DistributedNodeConfig { + DistributedNodeConfig { + swim: SwimConfig { + probe_interval: 1, + probe_timeout: 3, + indirect_probes: 1, + suspicion_timeout: 5, + dead_reprobe_interval: 0, + }, + cache_capacity: 100, + republish_interval: 50, + registry: RegistryConfig::default(), + } +} + +fn make_driver() -> IrohDriver { + IrohDriver::new(IrohDriverConfig { + secret_key: None, + relay_mode: RelayMode::Disabled, + node: test_config(), + }) + .expect("failed to create iroh driver") +} + +#[test] +fn iroh_driver_creates_with_unique_identity() { + let d1 = make_driver(); + let d2 = make_driver(); + assert_ne!(d1.node_id(), d2.node_id()); + d1.shutdown(); + d2.shutdown(); +} + +#[test] +fn iroh_driver_snapshot_contains_node_id() { + let driver = make_driver(); + let snap = driver.snapshot(); + assert!(!snap.node_id.is_empty()); + assert_eq!(snap.members.len(), 0); + driver.shutdown(); +} + +#[test] +fn iroh_driver_identity_matches_iroh_endpoint() { + let driver = make_driver(); + let node_id = driver.node_id(); + // The snapshot's node_id hex should match the NodeId bytes + let snap = driver.snapshot(); + let expected_hex: String = node_id.0.iter().map(|b| format!("{:02x}", b)).collect(); + assert_eq!(snap.node_id, expected_hex); + driver.shutdown(); +} diff --git a/crates/distribution/tests/kademlia_lookup.rs b/crates/distribution/tests/kademlia_lookup.rs index 169dbe9..c420519 100644 --- a/crates/distribution/tests/kademlia_lookup.rs +++ b/crates/distribution/tests/kademlia_lookup.rs @@ -1,6 +1,3 @@ -use std::collections::HashMap; -use std::net::SocketAddr; - use distribution::kademlia::lookup::{LookupAction, NodeLookup}; use distribution::kademlia::routing_table::RoutingTable; use distribution::types::NodeId; @@ -9,18 +6,14 @@ fn node(byte: u8) -> NodeId { NodeId([byte; 32]) } -fn addr(port: u16) -> SocketAddr { - format!("127.0.0.1:{port}").parse().unwrap() -} - // ─── Basic lookup ─────────────────────────────────────────────────────────── #[test] fn lookup_queries_closest_seeds_first() { let mut rt = RoutingTable::with_k(node(0), 20); - rt.insert(node(1), addr(8001)); - rt.insert(node(2), addr(8002)); - rt.insert(node(3), addr(8003)); + rt.insert(node(1)); + rt.insert(node(2)); + rt.insert(node(3)); let target = node(0x10); let (lookup, actions) = NodeLookup::start_with_params(target, &rt, 3, 3); @@ -37,8 +30,8 @@ fn lookup_queries_closest_seeds_first() { #[test] fn lookup_terminates_when_no_new_closer_nodes() { let mut rt = RoutingTable::with_k(node(0), 3); - rt.insert(node(1), addr(8001)); - rt.insert(node(2), addr(8002)); + rt.insert(node(1)); + rt.insert(node(2)); let target = node(0x10); let (mut lookup, _initial_actions) = NodeLookup::start_with_params(target, &rt, 3, 3); @@ -56,15 +49,15 @@ fn lookup_terminates_when_no_new_closer_nodes() { #[test] fn lookup_discovers_closer_nodes_through_responses() { let mut rt = RoutingTable::with_k(node(0), 3); - rt.insert(node(1), addr(8001)); + rt.insert(node(1)); let target = node(0x10); let (mut lookup, _) = NodeLookup::start_with_params(target, &rt, 3, 3); // node(1) responds with closer nodes let actions = lookup.handle_response(node(1), vec![ - (node(0x11), addr(8011)), // very close to target 0x10 - (node(0x12), addr(8012)), + node(0x11), // very close to target 0x10 + node(0x12), ]); // Should query the newly discovered closer nodes @@ -84,7 +77,7 @@ fn lookup_result_contains_k_closest() { for i in 1..=10u8 { let mut bytes = [0u8; 32]; bytes[0] = i; - rt.insert(NodeId(bytes), addr(8000 + i as u16)); + rt.insert(NodeId(bytes)); } let target = node(0x05); @@ -113,9 +106,9 @@ fn lookup_result_contains_k_closest() { #[test] fn lookup_handles_node_failures() { let mut rt = RoutingTable::with_k(node(0), 20); - rt.insert(node(1), addr(8001)); - rt.insert(node(2), addr(8002)); - rt.insert(node(3), addr(8003)); + rt.insert(node(1)); + rt.insert(node(2)); + rt.insert(node(3)); let target = node(0x10); let (mut lookup, _) = NodeLookup::start_with_params(target, &rt, 3, 3); @@ -147,7 +140,7 @@ fn lookup_with_empty_routing_table_completes_immediately() { fn lookup_converges_through_multiple_hops() { // Simulate: node 0 → knows node 1 → knows node 2 → knows node 3 (closest to target) let mut rt = RoutingTable::with_k(node(0), 20); - rt.insert(node(1), addr(8001)); + rt.insert(node(1)); let target = NodeId([0xFF; 32]); let (mut lookup, initial) = NodeLookup::start_with_params(target, &rt, 3, 3); @@ -156,14 +149,14 @@ fn lookup_converges_through_multiple_hops() { assert!(initial.iter().any(|a| matches!(a, LookupAction::Query { node_id, .. } if *node_id == node(1)))); // node 1 returns node 2 - let actions = lookup.handle_response(node(1), vec![(node(2), addr(8002))]); + let actions = lookup.handle_response(node(1), vec![node(2)]); assert!(actions.iter().any(|a| matches!(a, LookupAction::Query { node_id, .. } if *node_id == node(2)))); // node 2 returns node 3 (very close to target) let mut close_bytes = [0xFFu8; 32]; close_bytes[31] = 0xFE; let close_node = NodeId(close_bytes); - let actions = lookup.handle_response(node(2), vec![(close_node, addr(8003))]); + let actions = lookup.handle_response(node(2), vec![close_node]); // Should query the close node assert!(actions.iter().any(|a| matches!(a, LookupAction::Query { node_id, .. } if *node_id == close_node))); @@ -174,6 +167,6 @@ fn lookup_converges_through_multiple_hops() { // The done result should include the close node if let Some(LookupAction::Done { closest }) = actions.iter().find(|a| matches!(a, LookupAction::Done { .. })) { - assert!(closest.iter().any(|(id, _)| *id == close_node)); + assert!(closest.iter().any(|id| *id == close_node)); } } diff --git a/crates/distribution/tests/kademlia_routing.rs b/crates/distribution/tests/kademlia_routing.rs index 5218e58..8392728 100644 --- a/crates/distribution/tests/kademlia_routing.rs +++ b/crates/distribution/tests/kademlia_routing.rs @@ -5,16 +5,12 @@ fn node(byte: u8) -> NodeId { NodeId([byte; 32]) } -fn addr(port: u16) -> std::net::SocketAddr { - format!("127.0.0.1:{port}").parse().unwrap() -} - // ─── Basic operations ─────────────────────────────────────────────────────── #[test] fn insert_and_contains() { let mut rt = RoutingTable::new(node(0)); - assert!(rt.insert(node(1), addr(8001))); + assert!(rt.insert(node(1))); assert!(rt.contains(&node(1))); assert!(!rt.contains(&node(2))); } @@ -22,14 +18,14 @@ fn insert_and_contains() { #[test] fn insert_self_is_rejected() { let mut rt = RoutingTable::new(node(0)); - assert!(!rt.insert(node(0), addr(8000))); + assert!(!rt.insert(node(0))); assert_eq!(rt.len(), 0); } #[test] fn remove_node() { let mut rt = RoutingTable::new(node(0)); - rt.insert(node(1), addr(8001)); + rt.insert(node(1)); assert!(rt.remove(&node(1))); assert!(!rt.contains(&node(1))); assert_eq!(rt.len(), 0); @@ -44,10 +40,10 @@ fn remove_nonexistent_returns_false() { #[test] fn duplicate_insert_updates_position() { let mut rt = RoutingTable::new(node(0)); - rt.insert(node(1), addr(8001)); - rt.insert(node(2), addr(8002)); + rt.insert(node(1)); + rt.insert(node(2)); // Re-insert node 1 — should move to most-recently-seen - assert!(rt.insert(node(1), addr(8001))); + assert!(rt.insert(node(1))); assert_eq!(rt.len(), 2); } @@ -62,7 +58,7 @@ fn closest_returns_k_nearest_by_xor() { for i in 1..=10u8 { let mut bytes = [0u8; 32]; bytes[0] = i; - rt.insert(NodeId(bytes), addr(8000 + i as u16)); + rt.insert(NodeId(bytes)); } let target = NodeId([0x00; 32]); // same as self, closest by XOR @@ -78,8 +74,8 @@ fn closest_returns_k_nearest_by_xor() { #[test] fn closest_returns_all_when_fewer_than_count() { let mut rt = RoutingTable::new(node(0)); - rt.insert(node(1), addr(8001)); - rt.insert(node(2), addr(8002)); + rt.insert(node(1)); + rt.insert(node(2)); let closest = rt.closest(&node(0), 10); assert_eq!(closest.len(), 2); @@ -93,12 +89,12 @@ fn closest_to_specific_target() { // Node A: XOR distance to target [0xFF...] is [0xFF ^ 0x01, ...] = [0xFE, ...] let mut a = [0u8; 32]; a[0] = 0x01; - rt.insert(NodeId(a), addr(8001)); + rt.insert(NodeId(a)); // Node B: XOR distance to target [0xFF...] is [0xFF ^ 0xFE, ...] = [0x01, ...] let mut b = [0u8; 32]; b[0] = 0xFE; - rt.insert(NodeId(b), addr(8002)); + rt.insert(NodeId(b)); let target = NodeId([0xFF; 32]); let closest = rt.closest(&target, 1); @@ -121,9 +117,9 @@ fn bucket_overflow_goes_to_replacement_cache() { let mut bytes_b = [0u8; 32]; bytes_b[0] = 0xC0; let mut bytes_c = [0u8; 32]; bytes_c[0] = 0xA0; - assert!(rt.insert(NodeId(bytes_a), addr(8001))); // fits - assert!(rt.insert(NodeId(bytes_b), addr(8002))); // fits - assert!(!rt.insert(NodeId(bytes_c), addr(8003))); // goes to replacement + assert!(rt.insert(NodeId(bytes_a))); // fits + assert!(rt.insert(NodeId(bytes_b))); // fits + assert!(!rt.insert(NodeId(bytes_c))); // goes to replacement assert_eq!(rt.len(), 2); assert!(rt.contains(&NodeId(bytes_a))); @@ -140,9 +136,9 @@ fn removing_node_promotes_from_replacement() { let mut bytes_b = [0u8; 32]; bytes_b[0] = 0xC0; let mut bytes_c = [0u8; 32]; bytes_c[0] = 0xA0; - rt.insert(NodeId(bytes_a), addr(8001)); - rt.insert(NodeId(bytes_b), addr(8002)); - rt.insert(NodeId(bytes_c), addr(8003)); // replacement + rt.insert(NodeId(bytes_a)); + rt.insert(NodeId(bytes_b)); + rt.insert(NodeId(bytes_c)); // replacement // Remove A — C should be promoted rt.remove(&NodeId(bytes_a)); @@ -171,7 +167,7 @@ fn closest_ordering_is_stable_with_many_nodes() { let mut bytes = [0u8; 32]; bytes[0] = i; bytes[1] = i.wrapping_mul(37); - rt.insert(NodeId(bytes), addr(8000 + i as u16)); + rt.insert(NodeId(bytes)); } let target = NodeId([0x10; 32]); diff --git a/crates/distribution/tests/node_integration.rs b/crates/distribution/tests/node_integration.rs index 6982213..361a574 100644 --- a/crates/distribution/tests/node_integration.rs +++ b/crates/distribution/tests/node_integration.rs @@ -3,107 +3,27 @@ //! These tests verify the full composed behavior from a consumer's perspective: //! cluster formation, actor registration/resolution, and fault tolerance. -use std::net::SocketAddr; +mod common; use swactor::actor::ActorAddress; +use common::{test_config, TestCluster}; use distribution::crypto::Keypair; use distribution::node::{DistributedNode, DistributedNodeConfig, ResolveResult}; use distribution::swim::node::NodeAction; -use distribution::registry::RegistryConfig; -use distribution::swim::probe::SwimConfig; -use distribution::types::NodeId; - -fn test_config(addr: &str) -> DistributedNodeConfig { - DistributedNodeConfig { - listen_addr: addr.parse().unwrap(), - swim: SwimConfig { - probe_interval: 1, - probe_timeout: 3, - indirect_probes: 1, - suspicion_timeout: 5, - dead_reprobe_interval: 0, - }, - cache_capacity: 100, - republish_interval: 50, - registry: RegistryConfig::default(), - } -} - -/// Simulate a network round: deliver actions from `sender` to the appropriate -/// `receiver` node. Returns any actions generated by the receiver. -fn deliver_actions( - actions: &[NodeAction], - sender_id: NodeId, - sender_addr: SocketAddr, - nodes: &mut [(NodeId, SocketAddr, &mut DistributedNode)], -) -> Vec { - let mut responses = Vec::new(); - for action in actions { - match action { - NodeAction::SendPing { to, sequence, piggyback, .. } => { - if let Some((_, _, node)) = nodes.iter_mut().find(|(id, _, _)| id == to) { - responses.extend(node.handle_ping(sender_id, sender_addr, *sequence, piggyback)); - } - } - NodeAction::SendAck { to, sequence, piggyback, .. } => { - if let Some((_, _, node)) = nodes.iter_mut().find(|(id, _, _)| id == to) { - responses.extend(node.handle_ack(sender_id, *sequence, piggyback)); - } - } - NodeAction::SendJoinRequest { to_addr } => { - if let Some((_, _, node)) = nodes.iter_mut().find(|(_, addr, _)| addr == to_addr) { - responses.extend(node.handle_join_request(sender_id, sender_addr)); - } - } - NodeAction::SendJoinResponse { to, members, .. } => { - if let Some((_, _, node)) = nodes.iter_mut().find(|(id, _, _)| id == to) { - responses.extend(node.handle_join_response(members.clone())); - } - } - NodeAction::SendPingReq { relay, target, target_addr, sequence, piggyback, .. } => { - if let Some((_, _, node)) = nodes.iter_mut().find(|(id, _, _)| id == relay) { - responses.extend(node.handle_ping_req(sender_id, *target, *target_addr, *sequence, piggyback)); - } - } - NodeAction::MembershipChanged { .. } => { - // Notifications — no delivery needed - } - } - } - responses -} // ─── Cluster Formation ─────────────────────────────────────────────────────── #[test] fn two_node_cluster_forms_via_join() { // Given: a seed node and a joining node - let mut seed = DistributedNode::new(test_config("127.0.0.1:9001")); - let mut joiner = DistributedNode::new(test_config("127.0.0.1:9002")); + let cluster = TestCluster::new(2); - let seed_id = seed.node_id(); - let seed_addr = seed.listen_addr(); - let joiner_id = joiner.node_id(); - let joiner_addr = joiner.listen_addr(); - - // When: the joiner sends a join request to the seed - let join_actions = joiner.join(&[seed_addr]); - - // Deliver join request to seed - let mut all_nodes: Vec<(NodeId, SocketAddr, &mut DistributedNode)> = vec![ - (seed_id, seed_addr, &mut seed), - ]; - let responses = deliver_actions(&join_actions, joiner_id, joiner_addr, &mut all_nodes); - - // Deliver join response back to joiner - let mut all_nodes: Vec<(NodeId, SocketAddr, &mut DistributedNode)> = vec![ - (joiner_id, joiner_addr, &mut joiner), - ]; - let _ = deliver_actions(&responses, seed_id, seed_addr, &mut all_nodes); + let seed_id = cluster.node_id(0); + let joiner_id = cluster.node_id(1); // Then: both nodes see each other as members - let seed_members = seed.members(); - let joiner_members = joiner.members(); + let seed_members = cluster[0].members(); + let joiner_members = cluster[1].members(); assert!( seed_members.iter().any(|m| m.node_id == joiner_id), @@ -118,24 +38,13 @@ fn two_node_cluster_forms_via_join() { #[test] fn joined_node_appears_in_routing_table() { // Given: two nodes that have formed a cluster - let mut seed = DistributedNode::new(test_config("127.0.0.1:9011")); - let mut joiner = DistributedNode::new(test_config("127.0.0.1:9012")); + let cluster = TestCluster::new(2); - let seed_id = seed.node_id(); - let seed_addr = seed.listen_addr(); - let joiner_id = joiner.node_id(); - let joiner_addr = joiner.listen_addr(); - - // When: join completes - let actions = joiner.join(&[seed_addr]); - let mut nodes = vec![(seed_id, seed_addr, &mut seed)]; - let responses = deliver_actions(&actions, joiner_id, joiner_addr, &mut nodes); - let mut nodes = vec![(joiner_id, joiner_addr, &mut joiner)]; - let _ = deliver_actions(&responses, seed_id, seed_addr, &mut nodes); + let seed_id = cluster.node_id(0); // Then: joiner's routing table contains the seed assert!( - joiner.routing_table().contains(&seed_id), + cluster[1].routing_table().contains(&seed_id), "joiner's routing table should contain seed" ); } @@ -145,7 +54,7 @@ fn joined_node_appears_in_routing_table() { #[test] fn registered_actor_resolves_from_cache() { // Given: a node with a registered actor - let mut node = DistributedNode::new(test_config("127.0.0.1:9021")); + let mut node = DistributedNode::new(test_config()); let actor = ActorAddress::new_random(); let node_id = node.node_id(); @@ -164,30 +73,20 @@ fn registered_actor_resolves_from_cache() { #[test] fn unknown_actor_returns_needs_lookup_when_peers_known() { // Given: a two-node cluster - let mut seed = DistributedNode::new(test_config("127.0.0.1:9031")); - let mut joiner = DistributedNode::new(test_config("127.0.0.1:9032")); + let mut cluster = TestCluster::new(2); - let seed_id = seed.node_id(); - let seed_addr = seed.listen_addr(); - let joiner_id = joiner.node_id(); - let joiner_addr = joiner.listen_addr(); - - let actions = joiner.join(&[seed_addr]); - let mut nodes = vec![(seed_id, seed_addr, &mut seed)]; - let responses = deliver_actions(&actions, joiner_id, joiner_addr, &mut nodes); - let mut nodes = vec![(joiner_id, joiner_addr, &mut joiner)]; - let _ = deliver_actions(&responses, seed_id, seed_addr, &mut nodes); + let seed_id = cluster.node_id(0); // When: resolving an unregistered actor on the joiner let unknown_actor = ActorAddress::new_random(); - let result = joiner.resolve_actor(&unknown_actor); + let result = cluster[1].resolve_actor(&unknown_actor); // Then: it returns NeedsLookup with the seed as a closest node match result { ResolveResult::NeedsLookup { closest_nodes } => { assert!(!closest_nodes.is_empty(), "should suggest nodes to query"); assert!( - closest_nodes.iter().any(|(id, _)| *id == seed_id), + closest_nodes.iter().any(|id| *id == seed_id), "should include seed as a closest node" ); } @@ -198,7 +97,7 @@ fn unknown_actor_returns_needs_lookup_when_peers_known() { #[test] fn unknown_actor_returns_not_found_when_no_peers() { // Given: an isolated node with no peers - let mut node = DistributedNode::new(test_config("127.0.0.1:9041")); + let mut node = DistributedNode::new(test_config()); // When: resolving an unknown actor let result = node.resolve_actor(&ActorAddress::new_random()); @@ -211,7 +110,7 @@ fn unknown_actor_returns_not_found_when_no_peers() { fn store_remote_directory_entry_makes_it_resolvable() { // Given: node B receives a signed directory entry from node A let kp_a = Keypair::generate(); - let mut node_b = DistributedNode::new(test_config("127.0.0.1:9051")); + let mut node_b = DistributedNode::new(test_config()); let actor = ActorAddress::new_random(); let entry = kp_a.sign_directory_entry(actor, 1); @@ -234,31 +133,20 @@ fn store_remote_directory_entry_makes_it_resolvable() { #[test] fn cache_invalidation_forces_re_lookup() { // Given: a node with a cached actor location and peers in routing table - let mut seed = DistributedNode::new(test_config("127.0.0.1:9061")); - let mut node = DistributedNode::new(test_config("127.0.0.1:9062")); + let mut cluster = TestCluster::new(2); - let seed_id = seed.node_id(); - let seed_addr = seed.listen_addr(); - let node_id = node.node_id(); - let node_addr = node.listen_addr(); - - // Form cluster - let actions = node.join(&[seed_addr]); - let mut nodes = vec![(seed_id, seed_addr, &mut seed)]; - let responses = deliver_actions(&actions, node_id, node_addr, &mut nodes); - let mut nodes = vec![(node_id, node_addr, &mut node)]; - let _ = deliver_actions(&responses, seed_id, seed_addr, &mut nodes); + let node_id = cluster.node_id(1); // Register and resolve an actor (populates cache) let actor = ActorAddress::new_random(); - node.register_actor(actor, 1); - assert!(matches!(node.resolve_actor(&actor), ResolveResult::Cached(_))); + cluster[1].register_actor(actor, 1); + assert!(matches!(cluster[1].resolve_actor(&actor), ResolveResult::Cached(_))); // When: the cache is invalidated (e.g., delivery failure) - node.invalidate_cache(&actor); + cluster[1].invalidate_cache(&actor); // Then: next resolve falls through to directory (still finds it there) - match node.resolve_actor(&actor) { + match cluster[1].resolve_actor(&actor) { ResolveResult::Cached(resolved) => { assert_eq!(resolved, node_id, "should re-populate from local directory"); } @@ -272,12 +160,11 @@ fn cache_invalidation_forces_re_lookup() { fn node_death_clears_routing_table_and_cache_entries() { // Given: a node that has a peer in its routing table and cache entries for that peer let kp_peer = Keypair::generate(); - let mut node = DistributedNode::new(test_config("127.0.0.1:9071")); + let mut node = DistributedNode::new(test_config()); let peer_id = kp_peer.node_id(); - let peer_addr: SocketAddr = "127.0.0.1:9072".parse().unwrap(); // Simulate peer being known: handle a join so it's in routing table + members - let _ = node.handle_join_request(peer_id, peer_addr); + let _ = node.handle_join_request(peer_id); // Store a directory entry from the peer let actor = ActorAddress::new_random(); @@ -286,15 +173,6 @@ fn node_death_clears_routing_table_and_cache_entries() { // Resolve to populate cache let _ = node.resolve_actor(&actor); - // When: a tick produces a MembershipChanged(Dead) for that peer - // We simulate this by directly calling handle_membership_change via tick - // that produces the death notification. - // For a more direct test, we verify through the tick + SWIM mechanism. - // - // Since we can't easily drive SWIM to produce a Death in a unit test - // without many rounds, let's verify the routing table state directly - // after wiring through the public API. - assert!(node.routing_table().contains(&peer_id), "peer should be in routing table initially"); // We can verify the wiring by checking that after node death handling, @@ -309,23 +187,11 @@ fn node_death_clears_routing_table_and_cache_entries() { #[test] fn graceful_leave_disseminates_death_on_next_probe() { // Given: a two-node cluster - let mut seed = DistributedNode::new(test_config("127.0.0.1:9081")); - let mut node = DistributedNode::new(test_config("127.0.0.1:9082")); - - let seed_id = seed.node_id(); - let seed_addr = seed.listen_addr(); - let node_id = node.node_id(); - let node_addr = node.listen_addr(); - - let actions = node.join(&[seed_addr]); - let mut nodes = vec![(seed_id, seed_addr, &mut seed)]; - let responses = deliver_actions(&actions, node_id, node_addr, &mut nodes); - let mut nodes = vec![(node_id, node_addr, &mut node)]; - let _ = deliver_actions(&responses, seed_id, seed_addr, &mut nodes); + let mut cluster = TestCluster::new(2); // When: the node leaves and then ticks (probe carries piggybacked death) - let _leave_actions = node.leave(); - let tick_actions = node.tick(); + let _leave_actions = cluster[1].leave(); + let tick_actions = cluster[1].tick(); // Then: the tick produces a ping that carries the death piggyback // The ping's piggyback will contain the node's self-death update @@ -343,22 +209,10 @@ fn graceful_leave_disseminates_death_on_next_probe() { #[test] fn tick_produces_swim_probe_actions_when_peers_present() { // Given: a two-node cluster - let mut seed = DistributedNode::new(test_config("127.0.0.1:9091")); - let mut node = DistributedNode::new(test_config("127.0.0.1:9092")); - - let seed_id = seed.node_id(); - let seed_addr = seed.listen_addr(); - let node_id = node.node_id(); - let node_addr = node.listen_addr(); - - let actions = node.join(&[seed_addr]); - let mut nodes = vec![(seed_id, seed_addr, &mut seed)]; - let responses = deliver_actions(&actions, node_id, node_addr, &mut nodes); - let mut nodes = vec![(node_id, node_addr, &mut node)]; - let _ = deliver_actions(&responses, seed_id, seed_addr, &mut nodes); + let mut cluster = TestCluster::new(2); // When: ticking the node (with probe_interval=1, so first tick triggers a probe) - let tick_actions = node.tick(); + let tick_actions = cluster[1].tick(); // Then: it produces probe actions (pings to known members) let has_ping = tick_actions.iter().any(|a| matches!(a, NodeAction::SendPing { .. })); @@ -372,7 +226,7 @@ fn registered_actor_is_tracked_for_republish() { // Given: a node with a registered actor let mut node = DistributedNode::new(DistributedNodeConfig { republish_interval: 3, - ..test_config("127.0.0.1:9101") + ..test_config() }); let actor = ActorAddress::new_random(); node.register_actor(actor, 1); diff --git a/crates/distribution/tests/registry.rs b/crates/distribution/tests/registry.rs index 2ab652f..7545c5c 100644 --- a/crates/distribution/tests/registry.rs +++ b/crates/distribution/tests/registry.rs @@ -1,123 +1,21 @@ //! Behavioral tests for the cluster registry. //! -//! Tests gossip-propagated naming via LWW-Register CRDT, using the same -//! `deliver_actions` + `test_config` pattern from `node_integration.rs`. +//! Tests gossip-propagated naming via LWW-Register CRDT, using the shared +//! `TestCluster` harness from `common`. -use std::net::SocketAddr; +mod common; use swactor::actor::ActorAddress; -use distribution::node::{DistributedNode, DistributedNodeConfig}; +use common::{test_config, TestCluster}; +use distribution::node::DistributedNode; use distribution::registry::{ClusterRegistry, RegistryConfig, RegistryEntry, RegistryEvent}; -use distribution::swim::node::NodeAction; -use distribution::swim::probe::SwimConfig; use distribution::types::NodeId; -fn test_config(addr: &str) -> DistributedNodeConfig { - DistributedNodeConfig { - listen_addr: addr.parse().unwrap(), - swim: SwimConfig { - probe_interval: 1, - probe_timeout: 3, - indirect_probes: 1, - suspicion_timeout: 5, - dead_reprobe_interval: 0, - }, - cache_capacity: 100, - republish_interval: 50, - registry: RegistryConfig::default(), - } -} - -/// Simulate a network round: deliver actions from `sender` to the appropriate -/// `receiver` node. Returns any actions generated by the receiver. -fn deliver_actions( - actions: &[NodeAction], - sender_id: NodeId, - sender_addr: SocketAddr, - nodes: &mut [(NodeId, SocketAddr, &mut DistributedNode)], -) -> Vec { - let mut responses = Vec::new(); - for action in actions { - match action { - NodeAction::SendPing { to, sequence, piggyback, .. } => { - if let Some((_, _, node)) = nodes.iter_mut().find(|(id, _, _)| id == to) { - responses.extend(node.handle_ping(sender_id, sender_addr, *sequence, piggyback)); - } - } - NodeAction::SendAck { to, sequence, piggyback, .. } => { - if let Some((_, _, node)) = nodes.iter_mut().find(|(id, _, _)| id == to) { - responses.extend(node.handle_ack(sender_id, *sequence, piggyback)); - } - } - NodeAction::SendJoinRequest { to_addr } => { - if let Some((_, _, node)) = nodes.iter_mut().find(|(_, addr, _)| addr == to_addr) { - responses.extend(node.handle_join_request(sender_id, sender_addr)); - } - } - NodeAction::SendJoinResponse { to, members, .. } => { - if let Some((_, _, node)) = nodes.iter_mut().find(|(id, _, _)| id == to) { - responses.extend(node.handle_join_response(members.clone())); - } - } - NodeAction::SendPingReq { relay, target, target_addr, sequence, piggyback, .. } => { - if let Some((_, _, node)) = nodes.iter_mut().find(|(id, _, _)| id == relay) { - responses.extend(node.handle_ping_req(sender_id, *target, *target_addr, *sequence, piggyback)); - } - } - NodeAction::MembershipChanged { .. } => {} - } - } - responses -} - -/// Form a two-node cluster, returning (node_a, node_b) and their ids/addrs. -fn form_cluster( - addr_a: &str, - addr_b: &str, -) -> (DistributedNode, NodeId, SocketAddr, DistributedNode, NodeId, SocketAddr) { - let mut a = DistributedNode::new(test_config(addr_a)); - let mut b = DistributedNode::new(test_config(addr_b)); - - let a_id = a.node_id(); - let a_addr = a.listen_addr(); - let b_id = b.node_id(); - let b_addr = b.listen_addr(); - - let actions = b.join(&[a_addr]); - let mut nodes = vec![(a_id, a_addr, &mut a)]; - let responses = deliver_actions(&actions, b_id, b_addr, &mut nodes); - let mut nodes = vec![(b_id, b_addr, &mut b)]; - let _ = deliver_actions(&responses, a_id, a_addr, &mut nodes); - - (a, a_id, a_addr, b, b_id, b_addr) -} - -/// Run several gossip rounds between two nodes. -fn gossip_rounds( - a: &mut DistributedNode, a_id: NodeId, a_addr: SocketAddr, - b: &mut DistributedNode, b_id: NodeId, b_addr: SocketAddr, - rounds: usize, -) { - for _ in 0..rounds { - let actions_a = a.tick(); - let mut nodes = vec![(b_id, b_addr, &mut *b)]; - let responses = deliver_actions(&actions_a, a_id, a_addr, &mut nodes); - let mut nodes = vec![(a_id, a_addr, &mut *a)]; - let _ = deliver_actions(&responses, b_id, b_addr, &mut nodes); - - let actions_b = b.tick(); - let mut nodes = vec![(a_id, a_addr, &mut *a)]; - let responses = deliver_actions(&actions_b, b_id, b_addr, &mut nodes); - let mut nodes = vec![(b_id, b_addr, &mut *b)]; - let _ = deliver_actions(&responses, a_id, a_addr, &mut nodes); - } -} - // ─── Test 1: register and resolve ─────────────────────────────────────────── #[test] fn register_and_resolve() { - let mut node = DistributedNode::new(test_config("127.0.0.1:10001")); + let mut node = DistributedNode::new(test_config()); let actor = ActorAddress::new_random(); let node_id = node.node_id(); @@ -131,7 +29,7 @@ fn register_and_resolve() { #[test] fn unregistered_name_returns_none() { - let node = DistributedNode::new(test_config("127.0.0.1:10002")); + let node = DistributedNode::new(test_config()); assert_eq!(node.resolve_name("nonexistent"), None); } @@ -139,7 +37,7 @@ fn unregistered_name_returns_none() { #[test] fn unregister_tombstones_name() { - let mut node = DistributedNode::new(test_config("127.0.0.1:10003")); + let mut node = DistributedNode::new(test_config()); let actor = ActorAddress::new_random(); node.register_name("service".into(), actor); @@ -153,7 +51,7 @@ fn unregister_tombstones_name() { #[test] fn re_registration_updates_binding() { - let mut node = DistributedNode::new(test_config("127.0.0.1:10004")); + let mut node = DistributedNode::new(test_config()); let actor_a = ActorAddress::new_random(); let actor_b = ActorAddress::new_random(); let node_id = node.node_id(); @@ -260,133 +158,89 @@ fn lww_tiebreak_generation_then_node_id() { #[test] fn gossip_propagates_registration() { - let (mut a, a_id, a_addr, mut b, b_id, b_addr) = - form_cluster("127.0.0.1:10010", "127.0.0.1:10011"); + let mut cluster = TestCluster::new(2); let actor = ActorAddress::new_random(); - a.register_name("greeter".into(), actor); + cluster[0].register_name("greeter".into(), actor); // B doesn't know about "greeter" yet. - assert_eq!(b.resolve_name("greeter"), None); + assert_eq!(cluster[1].resolve_name("greeter"), None); // Run gossip rounds — registry entries piggyback on SWIM messages. - gossip_rounds(&mut a, a_id, a_addr, &mut b, b_id, b_addr, 5); + cluster.gossip_rounds(5); // Now B should resolve "greeter" to A's actor. - assert_eq!(b.resolve_name("greeter"), Some((actor, a_id))); + let a_id = cluster.node_id(0); + assert_eq!(cluster[1].resolve_name("greeter"), Some((actor, a_id))); } // ─── Test 8: tombstone propagation via gossip ─────────────────────────────── #[test] fn tombstone_propagation_via_gossip() { - let (mut a, a_id, a_addr, mut b, b_id, b_addr) = - form_cluster("127.0.0.1:10020", "127.0.0.1:10021"); + let mut cluster = TestCluster::new(2); let actor = ActorAddress::new_random(); - a.register_name("ephemeral".into(), actor); + cluster[0].register_name("ephemeral".into(), actor); // Propagate the registration. - gossip_rounds(&mut a, a_id, a_addr, &mut b, b_id, b_addr, 5); - assert_eq!(b.resolve_name("ephemeral"), Some((actor, a_id))); + cluster.gossip_rounds(5); + let a_id = cluster.node_id(0); + assert_eq!(cluster[1].resolve_name("ephemeral"), Some((actor, a_id))); // Now unregister on A. - a.unregister_name("ephemeral"); + cluster[0].unregister_name("ephemeral"); // Propagate the tombstone. - gossip_rounds(&mut a, a_id, a_addr, &mut b, b_id, b_addr, 5); + cluster.gossip_rounds(5); - assert_eq!(b.resolve_name("ephemeral"), None); + assert_eq!(cluster[1].resolve_name("ephemeral"), None); } // ─── Test 9: node death tombstones entries ────────────────────────────────── #[test] fn node_death_tombstones_entries() { - // Set up a 3-node cluster: A, B, C - let mut a = DistributedNode::new(test_config("127.0.0.1:10030")); - let mut b = DistributedNode::new(test_config("127.0.0.1:10031")); - let mut c = DistributedNode::new(test_config("127.0.0.1:10032")); + // Set up a 3-node cluster: A(0), B(1), C(2) + let mut cluster = TestCluster::new(3); - let a_id = a.node_id(); - let a_addr = a.listen_addr(); - let b_id = b.node_id(); - let b_addr = b.listen_addr(); - let c_id = c.node_id(); - let c_addr = c.listen_addr(); - - // B and C join A. - let actions = b.join(&[a_addr]); - let mut nodes = vec![(a_id, a_addr, &mut a)]; - let responses = deliver_actions(&actions, b_id, b_addr, &mut nodes); - let mut nodes = vec![(b_id, b_addr, &mut b)]; - let _ = deliver_actions(&responses, a_id, a_addr, &mut nodes); - - let actions = c.join(&[a_addr]); - let mut nodes = vec![(a_id, a_addr, &mut a)]; - let responses = deliver_actions(&actions, c_id, c_addr, &mut nodes); - let mut nodes = vec![(c_id, c_addr, &mut c)]; - let _ = deliver_actions(&responses, a_id, a_addr, &mut nodes); + let b_id = cluster.node_id(1); // B registers a name. let actor = ActorAddress::new_random(); - b.register_name("b-service".into(), actor); + cluster[1].register_name("b-service".into(), actor); // Propagate B's registration to A and C via mesh gossip. - // B only knows A, so first B→A, then A→C carries it. - for _ in 0..5 { - // Each node ticks and delivers to all others. - let actions = b.tick(); - let mut nodes = vec![(a_id, a_addr, &mut a), (c_id, c_addr, &mut c)]; - let responses = deliver_actions(&actions, b_id, b_addr, &mut nodes); - let mut nodes = vec![(b_id, b_addr, &mut b)]; - let _ = deliver_actions(&responses, a_id, a_addr, &mut nodes); + cluster.gossip_rounds(5); - let actions = a.tick(); - let mut nodes = vec![(b_id, b_addr, &mut b), (c_id, c_addr, &mut c)]; - let responses = deliver_actions(&actions, a_id, a_addr, &mut nodes); - let mut nodes = vec![(a_id, a_addr, &mut a)]; - let _ = deliver_actions(&responses, b_id, b_addr, &mut nodes); + assert_eq!(cluster[0].resolve_name("b-service"), Some((actor, b_id))); + assert_eq!(cluster[2].resolve_name("b-service"), Some((actor, b_id))); - let actions = c.tick(); - let mut nodes = vec![(a_id, a_addr, &mut a), (b_id, b_addr, &mut b)]; - let responses = deliver_actions(&actions, c_id, c_addr, &mut nodes); - let mut nodes = vec![(c_id, c_addr, &mut c)]; - let _ = deliver_actions(&responses, a_id, a_addr, &mut nodes); - } - - assert_eq!(a.resolve_name("b-service"), Some((actor, b_id))); - assert_eq!(c.resolve_name("b-service"), Some((actor, b_id))); - - // B dies — SWIM detects via timeout. We simulate by ticking A many times - // without B responding, until suspicion_timeout expires. - for _ in 0..20 { - let actions = a.tick(); - // Don't deliver to B — it's "dead". Only deliver to C. - let mut nodes = vec![(c_id, c_addr, &mut c)]; - let responses = deliver_actions(&actions, a_id, a_addr, &mut nodes); - let mut nodes = vec![(a_id, a_addr, &mut a)]; - let _ = deliver_actions(&responses, c_id, c_addr, &mut nodes); - } + // B dies — SWIM detects via timeout. We simulate by running rounds + // without B participating, until suspicion_timeout expires. + cluster.gossip_rounds_excluding(&[1], 20); // After enough ticks, A should declare B dead, which tombstones "b-service". - // Note: exact timing depends on SWIM config, so we check both A and propagate to C. - let a_resolved = a.resolve_name("b-service"); + assert_eq!( + cluster[0].resolve_name("b-service"), + None, + "A must tombstone b-service after declaring B dead" + ); - if a_resolved.is_none() { - // A has tombstoned it — propagate to C. - gossip_rounds(&mut a, a_id, a_addr, &mut c, c_id, c_addr, 5); - assert_eq!(c.resolve_name("b-service"), None, "C should see tombstone after B's death propagates"); - } - // If SWIM hasn't declared death yet, the test still passes — the mechanism - // is wired, just needs more ticks. The important thing: no panics, clean flow. + // Propagate tombstone from A to C. + cluster.gossip_rounds_excluding(&[1], 5); + assert_eq!( + cluster[2].resolve_name("b-service"), + None, + "C should see tombstone after B's death propagates" + ); } // ─── Test 10: registry events emitted on change ───────────────────────────── #[test] fn registry_events_emitted_on_change() { - let mut node = DistributedNode::new(test_config("127.0.0.1:10040")); + let mut node = DistributedNode::new(test_config()); let actor = ActorAddress::new_random(); let node_id = node.node_id(); @@ -431,8 +285,6 @@ fn tombstone_gc_removes_old_tombstones() { assert_eq!(reg.tombstone_count(), 1); // Advance the clock past TTL by registering enough other things. - // Each register bumps the clock by 1, and we need clock to advance past - // tombstone.timestamp + tombstone_ttl. for i in 0..15 { let a = ActorAddress::new_random(); reg.register(format!("filler-{i}"), a, node_id, 1); @@ -454,56 +306,24 @@ fn tombstone_gc_removes_old_tombstones() { #[test] fn gossip_convergence_five_nodes() { - let base_port = 10050; - let mut nodes: Vec = (0..5) - .map(|i| { - DistributedNode::new(test_config(&format!("127.0.0.1:{}", base_port + i))) - }) - .collect(); - - // Collect ids/addrs before joining (borrow gymnastics). - let ids: Vec = nodes.iter().map(|n| n.node_id()).collect(); - let addrs: Vec = nodes.iter().map(|n| n.listen_addr()).collect(); - - // All join through node 0. - for i in 1..5 { - let actions = nodes[i].join(&[addrs[0]]); - // Deliver join request to node 0. - let mut target = vec![(ids[0], addrs[0], &mut nodes[0])]; - let responses = deliver_actions(&actions, ids[i], addrs[i], &mut target); - // Deliver join response back to node i. - let mut target = vec![(ids[i], addrs[i], &mut nodes[i])]; - let _ = deliver_actions(&responses, ids[0], addrs[0], &mut target); - } + let mut cluster = TestCluster::new(5); // Each node registers a unique name. let actors: Vec = (0..5).map(|_| ActorAddress::new_random()).collect(); for i in 0..5 { - nodes[i].register_name(format!("service-{i}"), actors[i]); + cluster[i].register_name(format!("service-{i}"), actors[i]); } - // Run many gossip rounds between all pairs. - for _round in 0..15 { - for i in 0..5 { - let tick_actions = nodes[i].tick(); - // Deliver to all other nodes. - for j in 0..5 { - if i == j { continue; } - let mut target = vec![(ids[j], addrs[j], &mut nodes[j])]; - let responses = deliver_actions(&tick_actions, ids[i], addrs[i], &mut target); - let mut target = vec![(ids[i], addrs[i], &mut nodes[i])]; - let _ = deliver_actions(&responses, ids[j], addrs[j], &mut target); - } - } - } + // Run many gossip rounds. + cluster.gossip_rounds(15); // All 5 names should be resolvable on all 5 nodes. for i in 0..5 { for j in 0..5 { - let result = nodes[i].resolve_name(&format!("service-{j}")); + let result = cluster[i].resolve_name(&format!("service-{j}")); assert_eq!( result, - Some((actors[j], ids[j])), + Some((actors[j], cluster.node_id(j))), "node {i} should resolve service-{j}" ); } diff --git a/crates/distribution/tests/swim_dissemination.rs b/crates/distribution/tests/swim_dissemination.rs index e4641ea..ee2bb6a 100644 --- a/crates/distribution/tests/swim_dissemination.rs +++ b/crates/distribution/tests/swim_dissemination.rs @@ -5,17 +5,13 @@ fn node(byte: u8) -> NodeId { NodeId([byte; 32]) } -fn addr(port: u16) -> std::net::SocketAddr { - format!("127.0.0.1:{port}").parse().unwrap() -} - // ─── Basic queue operations ───────────────────────────────────────────────── #[test] fn enqueue_and_take_single_update() { let mut q = DisseminationQueue::new(3); q.enqueue( - membership_update(node(1), addr(8001), MemberState::Alive, 0), + membership_update(node(1), MemberState::Alive, 0), 5, ); assert_eq!(q.len(), 1); @@ -30,7 +26,7 @@ fn take_respects_max_count() { let mut q = DisseminationQueue::new(3); for i in 1..=5 { q.enqueue( - membership_update(node(i), addr(8000 + i as u16), MemberState::Alive, 0), + membership_update(node(i), MemberState::Alive, 0), 10, ); } @@ -44,15 +40,15 @@ fn take_respects_max_count() { fn dead_updates_are_prioritized_over_suspect_and_alive() { let mut q = DisseminationQueue::new(3); q.enqueue( - membership_update(node(1), addr(8001), MemberState::Alive, 0), + membership_update(node(1), MemberState::Alive, 0), 10, ); q.enqueue( - membership_update(node(2), addr(8002), MemberState::Dead, 0), + membership_update(node(2), MemberState::Dead, 0), 10, ); q.enqueue( - membership_update(node(3), addr(8003), MemberState::Suspect, 0), + membership_update(node(3), MemberState::Suspect, 0), 10, ); @@ -69,7 +65,7 @@ fn entries_evicted_after_transmit_budget_exhausted() { // lambda=1, cluster_size=2 → budget = 1 * ceil(log2(2)) = 1 let mut q = DisseminationQueue::new(1); q.enqueue( - membership_update(node(1), addr(8001), MemberState::Alive, 0), + membership_update(node(1), MemberState::Alive, 0), 2, ); @@ -88,7 +84,7 @@ fn larger_cluster_gives_higher_transmit_budget() { // lambda=2, cluster_size=16 → budget = 2 * ceil(log2(16)) = 2 * 4 = 8 let mut q = DisseminationQueue::new(2); q.enqueue( - membership_update(node(1), addr(8001), MemberState::Alive, 0), + membership_update(node(1), MemberState::Alive, 0), 16, ); @@ -108,11 +104,11 @@ fn larger_cluster_gives_higher_transmit_budget() { fn newer_update_for_same_node_replaces_older() { let mut q = DisseminationQueue::new(3); q.enqueue( - membership_update(node(1), addr(8001), MemberState::Alive, 0), + membership_update(node(1), MemberState::Alive, 0), 10, ); q.enqueue( - membership_update(node(1), addr(8001), MemberState::Suspect, 0), + membership_update(node(1), MemberState::Suspect, 0), 10, ); @@ -125,12 +121,12 @@ fn newer_update_for_same_node_replaces_older() { fn higher_incarnation_replaces_lower() { let mut q = DisseminationQueue::new(3); q.enqueue( - membership_update(node(1), addr(8001), MemberState::Dead, 5), + membership_update(node(1), MemberState::Dead, 5), 10, ); // Same node, higher incarnation, Alive (incarnation wins over state) q.enqueue( - membership_update(node(1), addr(8001), MemberState::Alive, 6), + membership_update(node(1), MemberState::Alive, 6), 10, ); @@ -144,11 +140,11 @@ fn higher_incarnation_replaces_lower() { fn lower_incarnation_is_ignored() { let mut q = DisseminationQueue::new(3); q.enqueue( - membership_update(node(1), addr(8001), MemberState::Alive, 5), + membership_update(node(1), MemberState::Alive, 5), 10, ); q.enqueue( - membership_update(node(1), addr(8001), MemberState::Dead, 3), + membership_update(node(1), MemberState::Dead, 3), 10, ); @@ -163,11 +159,11 @@ fn lower_incarnation_is_ignored() { fn pack_and_unpack_piggyback_roundtrip() { let mut q = DisseminationQueue::new(3); q.enqueue( - membership_update(node(1), addr(8001), MemberState::Alive, 0), + membership_update(node(1), MemberState::Alive, 0), 10, ); q.enqueue( - membership_update(node(2), addr(8002), MemberState::Dead, 3), + membership_update(node(2), MemberState::Dead, 3), 10, ); diff --git a/crates/distribution/tests/swim_node.rs b/crates/distribution/tests/swim_node.rs index 93dcc73..23bb2cc 100644 --- a/crates/distribution/tests/swim_node.rs +++ b/crates/distribution/tests/swim_node.rs @@ -6,10 +6,6 @@ fn node(byte: u8) -> NodeId { NodeId([byte; 32]) } -fn addr(port: u16) -> std::net::SocketAddr { - format!("127.0.0.1:{port}").parse().unwrap() -} - fn fast_config() -> SwimConfig { SwimConfig { probe_interval: 5, @@ -32,45 +28,32 @@ fn tick_n(swim: &mut SwimNode, n: u64) -> Vec { #[test] fn solo_node_starts_with_empty_membership() { - let swim = SwimNode::new(node(0), addr(8000), fast_config()); + let swim = SwimNode::new(node(0), fast_config()); assert_eq!(swim.members().alive_count(), 0); } #[test] fn solo_node_ticks_without_actions() { - let mut swim = SwimNode::new(node(0), addr(8000), fast_config()); + let mut swim = SwimNode::new(node(0), fast_config()); let actions = tick_n(&mut swim, 100); assert!(actions.is_empty(), "no members → no actions"); } // ─── Join protocol ────────────────────────────────────────────────────────── -#[test] -fn join_produces_join_requests_to_seeds() { - let swim = SwimNode::new(node(1), addr(8001), fast_config()); - let seeds = vec![addr(8000), addr(8002)]; - let actions = swim.join(&seeds); - - assert_eq!(actions.len(), 2); - for action in &actions { - assert!(matches!(action, NodeAction::SendJoinRequest { .. })); - } -} - #[test] fn seed_handles_join_request_and_responds_with_members() { - let mut seed = SwimNode::new(node(0), addr(8000), fast_config()); + let mut seed = SwimNode::new(node(0), fast_config()); // Seed already knows about node 2 seed.handle_join_response(vec![NodeRecord { node_id: node(2), - addr: addr(8002), state: MemberState::Alive, incarnation: 0, }]); // Node 1 sends join request - let actions = seed.handle_join_request(node(1), addr(8001)); + let actions = seed.handle_join_request(node(1)); // Should have JoinResponse and MembershipChanged let join_responses: Vec<_> = actions @@ -90,18 +73,16 @@ fn seed_handles_join_request_and_responds_with_members() { #[test] fn joiner_populates_members_from_response() { - let mut joiner = SwimNode::new(node(1), addr(8001), fast_config()); + let mut joiner = SwimNode::new(node(1), fast_config()); let member_list = vec![ NodeRecord { node_id: node(2), - addr: addr(8002), state: MemberState::Alive, incarnation: 0, }, NodeRecord { node_id: node(3), - addr: addr(8003), state: MemberState::Alive, incarnation: 0, }, @@ -123,8 +104,8 @@ fn joiner_populates_members_from_response() { #[test] fn ping_produces_ack_response() { - let mut swim = SwimNode::new(node(0), addr(8000), fast_config()); - let actions = swim.handle_ping(node(1), addr(8001), 42, &[]); + let mut swim = SwimNode::new(node(0), fast_config()); + let actions = swim.handle_ping(node(1), 42, &[]); let acks: Vec<_> = actions.iter().filter(|a| matches!(a, NodeAction::SendAck { .. })).collect(); assert_eq!(acks.len(), 1); @@ -137,10 +118,10 @@ fn ping_produces_ack_response() { #[test] fn ping_from_unknown_node_adds_it_to_members() { - let mut swim = SwimNode::new(node(0), addr(8000), fast_config()); + let mut swim = SwimNode::new(node(0), fast_config()); assert_eq!(swim.members().alive_count(), 0); - swim.handle_ping(node(1), addr(8001), 1, &[]); + swim.handle_ping(node(1), 1, &[]); assert_eq!(swim.members().alive_count(), 1); } @@ -148,13 +129,12 @@ fn ping_from_unknown_node_adds_it_to_members() { #[test] fn membership_updates_piggyback_on_pings() { - let mut swim = SwimNode::new(node(0), addr(8000), fast_config()); + let mut swim = SwimNode::new(node(0), fast_config()); + // Join enqueues a membership update for dissemination. + swim.handle_join_request(node(1)); - // Add a member and join a node (which enqueues a dissemination update) - swim.handle_join_request(node(1), addr(8001)); - - // Tick until a probe fires — the ping should carry piggyback data - let actions = tick_n(&mut swim, 5); + // Tick past the probe interval — a ping must fire. + let actions = tick_n(&mut swim, 6); let pings: Vec<_> = actions.iter().filter_map(|a| { if let NodeAction::SendPing { piggyback, .. } = a { Some(piggyback) @@ -163,29 +143,29 @@ fn membership_updates_piggyback_on_pings() { } }).collect(); - if !pings.is_empty() { - // At least one ping should carry piggyback (the join update) - assert!(pings.iter().any(|pb| !pb.is_empty()), "pings should carry piggyback data"); - } + // With one member and probe_interval=5, at least one ping must fire. + assert!(!pings.is_empty(), "probe must fire within 6 ticks"); + // The join update must ride as piggyback on that ping. + assert!(pings.iter().any(|pb| !pb.is_empty()), "pings should carry piggyback data"); } // ─── Refutation ───────────────────────────────────────────────────────────── #[test] fn node_refutes_when_suspected_via_piggyback() { - let mut swim = SwimNode::new(node(0), addr(8000), fast_config()); + let mut swim = SwimNode::new(node(0), fast_config()); // Simulate receiving a piggyback that suspects us use distribution::swim::dissemination::{membership_update, DisseminationQueue}; let mut q = DisseminationQueue::new(3); q.enqueue( - membership_update(node(0), addr(8000), MemberState::Suspect, 0), + membership_update(node(0), MemberState::Suspect, 0), 5, ); let piggyback = q.pack_piggyback(10); // Receive a ping with this piggyback - swim.handle_ping(node(1), addr(8001), 1, &piggyback); + swim.handle_ping(node(1), 1, &piggyback); // Our incarnation should have been bumped assert!(swim.members().self_incarnation() > 0, "should have refuted by bumping incarnation"); @@ -195,13 +175,13 @@ fn node_refutes_when_suspected_via_piggyback() { #[test] fn leave_enqueues_death_for_dissemination() { - let mut swim = SwimNode::new(node(0), addr(8000), fast_config()); - swim.handle_join_request(node(1), addr(8001)); + let mut swim = SwimNode::new(node(0), fast_config()); + swim.handle_join_request(node(1)); swim.leave(); - // Tick to trigger a probe — the death update should piggyback - let actions = tick_n(&mut swim, 5); + // Tick past the probe interval — the death update must piggyback on the ping. + let actions = tick_n(&mut swim, 6); let pings_with_piggyback: Vec<_> = actions.iter().filter_map(|a| { if let NodeAction::SendPing { piggyback, .. } = a { if !piggyback.is_empty() { Some(piggyback) } else { None } @@ -210,21 +190,20 @@ fn leave_enqueues_death_for_dissemination() { } }).collect(); - // We can't guarantee the exact content, but the leave should enqueue something - // that gets piggybacked - assert!(!pings_with_piggyback.is_empty() || swim.members().alive_count() > 0); + assert!(!pings_with_piggyback.is_empty(), + "leave must enqueue a death update that piggybacks on the next ping"); } // ─── Full join scenario ───────────────────────────────────────────────────── #[test] fn three_node_cluster_forms_via_seed() { - let mut seed = SwimNode::new(node(0), addr(8000), fast_config()); - let mut n1 = SwimNode::new(node(1), addr(8001), fast_config()); - let mut n2 = SwimNode::new(node(2), addr(8002), fast_config()); + let mut seed = SwimNode::new(node(0), fast_config()); + let mut n1 = SwimNode::new(node(1), fast_config()); + let mut n2 = SwimNode::new(node(2), fast_config()); // Node 1 joins via seed - let join_actions = seed.handle_join_request(node(1), addr(8001)); + let join_actions = seed.handle_join_request(node(1)); for action in &join_actions { if let NodeAction::SendJoinResponse { members, .. } = action { n1.handle_join_response(members.clone()); @@ -232,7 +211,7 @@ fn three_node_cluster_forms_via_seed() { } // Node 2 joins via seed - let join_actions = seed.handle_join_request(node(2), addr(8002)); + let join_actions = seed.handle_join_request(node(2)); for action in &join_actions { if let NodeAction::SendJoinResponse { members, .. } = action { n2.handle_join_response(members.clone()); diff --git a/crates/distribution/tests/swim_probe.rs b/crates/distribution/tests/swim_probe.rs index d43be40..ad8fb08 100644 --- a/crates/distribution/tests/swim_probe.rs +++ b/crates/distribution/tests/swim_probe.rs @@ -1,5 +1,3 @@ -use std::net::SocketAddr; - use distribution::swim::member_list::MemberList; use distribution::swim::probe::{SwimAction, SwimConfig, SwimEvent, SwimProbe}; use distribution::types::{MemberState, NodeId}; @@ -8,10 +6,6 @@ fn node(byte: u8) -> NodeId { NodeId([byte; 32]) } -fn addr(port: u16) -> SocketAddr { - format!("127.0.0.1:{port}").parse().unwrap() -} - fn tick_n(probe: &mut SwimProbe, members: &mut MemberList, n: u64) -> Vec { let mut all_actions = Vec::new(); for _ in 0..n { @@ -25,7 +19,7 @@ fn tick_n(probe: &mut SwimProbe, members: &mut MemberList, n: u64) -> Vec(); @@ -116,7 +40,7 @@ fn distribution_codec_encodes_and_decodes_find_value_response() { match decoded { FindValueResponse::Closer(nodes) => { assert_eq!(nodes.len(), 2); - assert_eq!(nodes[0].0, NodeId([0x11; 32])); + assert_eq!(nodes[0], NodeId([0x11; 32])); } _ => panic!("expected Closer variant"), } @@ -150,55 +74,175 @@ fn all_message_types_registered_in_codec_registry() { } } -// ─── End-to-end: codec + TCP transport ────────────────────────────────────── +// ─── TCP transport tests (require "tcp" feature) ──────────────────────────── -#[test] -fn ping_message_survives_codec_and_tcp_roundtrip() { - let codecs = distribution_codec_registry(); +#[cfg(feature = "tcp")] +mod tcp_transport { + use swactor::actor::ActorAddress; + use swactor::transport::WireEnvelope; - let acceptor = TcpAcceptor::bind("127.0.0.1:0".parse().unwrap()).unwrap(); - let server_addr = acceptor.local_addr(); + use distribution::codec::distribution_codec_registry; + use distribution::messages::*; + use distribution::transport::{TcpAcceptor, TcpTransport}; + use distribution::types::NodeId; - let dest = ActorAddress::new_random(); - let ping = Ping { - from: NodeId([0xBB; 32]), - from_addr: "127.0.0.1:7001".parse().unwrap(), - sequence: 99, - piggyback: vec![], - }; + #[test] + fn wire_envelope_roundtrips_through_tcp() { + let acceptor = TcpAcceptor::bind("127.0.0.1:0".parse().unwrap()).unwrap(); + let addr = acceptor.local_addr(); - let type_id = std::any::TypeId::of::(); - let (tag, payload) = codecs.encode(type_id, Box::new(ping.clone())).unwrap(); + let original = WireEnvelope { + dest: ActorAddress::new_random(), + type_tag: "test::Msg".to_string(), + payload: vec![1, 2, 3, 4, 5], + }; - let envelope = WireEnvelope { - dest, - type_tag: tag, - payload, - }; + let original_clone = original.clone(); + let sender = std::thread::spawn(move || { + let transport = TcpTransport::new(addr); + transport.send_to(addr, original_clone).unwrap(); + }); - let envelope_clone = envelope.clone(); - let sender = std::thread::spawn(move || { - let transport = TcpTransport::new(server_addr); - transport.send_to(server_addr, envelope_clone).unwrap(); - }); + std::thread::sleep(std::time::Duration::from_millis(50)); + let mut streams = Vec::new(); + let envelopes = loop { + let envs = acceptor.try_recv(&mut streams); + if !envs.is_empty() { + break envs; + } + std::thread::sleep(std::time::Duration::from_millis(10)); + }; - std::thread::sleep(std::time::Duration::from_millis(50)); - let mut streams = Vec::new(); - let envelopes = loop { - let envs = acceptor.try_recv(&mut streams); - if !envs.is_empty() { - break envs; - } - std::thread::sleep(std::time::Duration::from_millis(10)); - }; + sender.join().unwrap(); - sender.join().unwrap(); + assert_eq!(envelopes.len(), 1); + let (received, _peer, hints) = &envelopes[0]; + assert_eq!(received.dest, original.dest); + assert_eq!(received.type_tag, original.type_tag); + assert_eq!(received.payload, original.payload); + assert!(hints.is_empty(), "no hints expected from plain send_to"); + } - let (received, _) = &envelopes[0]; + #[test] + fn wire_envelope_minimal_roundtrips() { + let acceptor = TcpAcceptor::bind("127.0.0.1:0".parse().unwrap()).unwrap(); + let addr = acceptor.local_addr(); - let (addr, msg_any) = codecs.receive(received.clone()).unwrap(); - assert_eq!(addr, dest); - let decoded: &Ping = msg_any.downcast_ref().unwrap(); - assert_eq!(decoded.from, NodeId([0xBB; 32])); - assert_eq!(decoded.sequence, 99); + let original = WireEnvelope { + dest: ActorAddress::new_random(), + type_tag: "test::Minimal".to_string(), + payload: vec![42], + }; + + let original_clone = original.clone(); + let sender = std::thread::spawn(move || { + let transport = TcpTransport::new(addr); + transport.send_to(addr, original_clone).unwrap(); + }); + + std::thread::sleep(std::time::Duration::from_millis(50)); + let mut streams = Vec::new(); + let envelopes = loop { + let envs = acceptor.try_recv(&mut streams); + if !envs.is_empty() { + break envs; + } + std::thread::sleep(std::time::Duration::from_millis(10)); + }; + + sender.join().unwrap(); + + let (received, _, _hints) = &envelopes[0]; + assert_eq!(received.payload, vec![42]); + } + + #[test] + fn two_drivers_complete_join_handshake() { + use distribution::driver::NodeDriver; + use distribution::node::DistributedNodeConfig; + + let mut driver_a = NodeDriver::new( + "127.0.0.1:0".parse().unwrap(), + DistributedNodeConfig::default(), + ) + .unwrap(); + + let mut driver_b = NodeDriver::new( + "127.0.0.1:0".parse().unwrap(), + DistributedNodeConfig::default(), + ) + .unwrap(); + + let b_addr = driver_b.listen_addr(); + + // A sends JoinRequest to B + driver_a.join(&[b_addr]); + + // B receives the JoinRequest, learns A's address from hints, sends JoinResponse + std::thread::sleep(std::time::Duration::from_millis(50)); + driver_b.recv(); + + // A receives the JoinResponse with member list + std::thread::sleep(std::time::Duration::from_millis(50)); + driver_a.recv(); + + // A should now have members — proving the full roundtrip worked. + // If hints were broken, B couldn't resolve A's address to send the + // JoinResponse, so A's member list would stay empty. + let members = driver_a.node().members(); + assert!( + !members.is_empty(), + "join handshake should complete: A needs members from JoinResponse" + ); + } + + #[test] + fn ping_message_survives_codec_and_tcp_roundtrip() { + let codecs = distribution_codec_registry(); + + let acceptor = TcpAcceptor::bind("127.0.0.1:0".parse().unwrap()).unwrap(); + let server_addr = acceptor.local_addr(); + + let dest = ActorAddress::new_random(); + let ping = Ping { + from: NodeId([0xBB; 32]), + sequence: 99, + piggyback: vec![], + }; + + let type_id = std::any::TypeId::of::(); + let (tag, payload) = codecs.encode(type_id, Box::new(ping.clone())).unwrap(); + + let envelope = WireEnvelope { + dest, + type_tag: tag, + payload, + }; + + let envelope_clone = envelope.clone(); + let sender = std::thread::spawn(move || { + let transport = TcpTransport::new(server_addr); + transport.send_to(server_addr, envelope_clone).unwrap(); + }); + + std::thread::sleep(std::time::Duration::from_millis(50)); + let mut streams = Vec::new(); + let envelopes = loop { + let envs = acceptor.try_recv(&mut streams); + if !envs.is_empty() { + break envs; + } + std::thread::sleep(std::time::Duration::from_millis(10)); + }; + + sender.join().unwrap(); + + let (received, _, _hints) = &envelopes[0]; + + let (addr, msg_any) = codecs.receive(received.clone()).unwrap(); + assert_eq!(addr, dest); + let decoded: &Ping = msg_any.downcast_ref().unwrap(); + assert_eq!(decoded.from, NodeId([0xBB; 32])); + assert_eq!(decoded.sequence, 99); + } } diff --git a/crates/distribution/tests/types_and_crypto.rs b/crates/distribution/tests/types_and_crypto.rs index ff368e9..4c1e804 100644 --- a/crates/distribution/tests/types_and_crypto.rs +++ b/crates/distribution/tests/types_and_crypto.rs @@ -131,7 +131,6 @@ fn node_record_serde_roundtrip() { let kp = Keypair::generate(); let record = NodeRecord { node_id: kp.node_id(), - addr: "127.0.0.1:8080".parse().unwrap(), state: MemberState::Alive, incarnation: 5, }; diff --git a/crates/runtime-dashboard/Cargo.toml b/crates/runtime-dashboard/Cargo.toml index 3f71d56..76fde7d 100644 --- a/crates/runtime-dashboard/Cargo.toml +++ b/crates/runtime-dashboard/Cargo.toml @@ -16,12 +16,15 @@ crossterm = { version = "0.28", optional = true } distribution = { path = "../distribution", optional = true } clap = { version = "4", features = ["derive"], optional = true } ctrlc = "3" +iroh = { version = "0.96", optional = true } [features] default = ["distribution"] tui = ["dep:ratatui", "dep:crossterm"] distribution = ["dep:distribution"] -node = ["distribution", "dep:clap", "swactor/transport"] +node = ["distribution", "dep:clap", "swactor/transport", "tcp"] +tcp = ["distribution/tcp"] +iroh = ["distribution/iroh", "dep:iroh"] [[bin]] name = "swactor-tui" diff --git a/crates/runtime-dashboard/examples/dashboard_demo.rs b/crates/runtime-dashboard/examples/dashboard_demo.rs index 9f039b3..f115b2f 100644 --- a/crates/runtime-dashboard/examples/dashboard_demo.rs +++ b/crates/runtime-dashboard/examples/dashboard_demo.rs @@ -1,4 +1,3 @@ -use std::net::SocketAddr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; @@ -86,7 +85,6 @@ impl DistributionStatsProvider for SnapshotProvider { fn tick_all_and_deliver( nodes: &mut [Option], node_ids: &[NodeId], - addrs: &[SocketAddr], ) { let n = nodes.len(); @@ -106,19 +104,15 @@ fn tick_all_and_deliver( let tagged_responses = deliver_actions_tagged( &actions, node_ids[sender_idx], - addrs[sender_idx], nodes, node_ids, - addrs, ); for (responder_idx, response_actions) in tagged_responses { deliver_actions_tagged( &response_actions, node_ids[responder_idx], - addrs[responder_idx], nodes, node_ids, - addrs, ); } } @@ -130,10 +124,8 @@ fn tick_all_and_deliver( fn deliver_actions_tagged( actions: &[NodeAction], sender_id: NodeId, - sender_addr: SocketAddr, nodes: &mut [Option], node_ids: &[NodeId], - node_addrs: &[SocketAddr], ) -> Vec<(usize, Vec)> { let mut tagged_responses: Vec<(usize, Vec)> = Vec::new(); @@ -143,12 +135,11 @@ fn deliver_actions_tagged( to, sequence, piggyback, - .. } => { if let Some(idx) = node_ids.iter().position(|id| id == to) { if let Some(ref mut node) = nodes[idx] { let resp = - node.handle_ping(sender_id, sender_addr, *sequence, piggyback); + node.handle_ping(sender_id, *sequence, piggyback); if !resp.is_empty() { tagged_responses.push((idx, resp)); } @@ -159,7 +150,6 @@ fn deliver_actions_tagged( to, sequence, piggyback, - .. } => { if let Some(idx) = node_ids.iter().position(|id| id == to) { if let Some(ref mut node) = nodes[idx] { @@ -170,16 +160,6 @@ fn deliver_actions_tagged( } } } - NodeAction::SendJoinRequest { to_addr } => { - if let Some(idx) = node_addrs.iter().position(|a| a == to_addr) { - if let Some(ref mut node) = nodes[idx] { - let resp = node.handle_join_request(sender_id, sender_addr); - if !resp.is_empty() { - tagged_responses.push((idx, resp)); - } - } - } - } NodeAction::SendJoinResponse { to, members, .. } => { if let Some(idx) = node_ids.iter().position(|id| id == to) { if let Some(ref mut node) = nodes[idx] { @@ -193,17 +173,14 @@ fn deliver_actions_tagged( NodeAction::SendPingReq { relay, target, - target_addr, sequence, piggyback, - .. } => { if let Some(idx) = node_ids.iter().position(|id| id == relay) { if let Some(ref mut node) = nodes[idx] { let resp = node.handle_ping_req( sender_id, *target, - *target_addr, *sequence, piggyback, ); @@ -222,6 +199,31 @@ fn deliver_actions_tagged( tagged_responses } +/// Simulate a join handshake: the joining node sends a join request to the +/// seed, and the seed's response is delivered back. +fn simulate_join( + joining_idx: usize, + seed_idx: usize, + nodes: &mut [Option], + node_ids: &[NodeId], +) { + let joining_id = node_ids[joining_idx]; + + // Seed handles the join request + let response_actions = if let Some(ref mut seed) = nodes[seed_idx] { + seed.handle_join_request(joining_id) + } else { + return; + }; + + // Deliver responses (SendJoinResponse) back to the joining node + let seed_id = node_ids[seed_idx]; + let tagged = deliver_actions_tagged(&response_actions, seed_id, nodes, node_ids); + for (responder_idx, response_actions) in tagged { + deliver_actions_tagged(&response_actions, node_ids[responder_idx], nodes, node_ids); + } +} + // ── Main ──────────────────────────────────────────────────────────────── fn main() { @@ -283,12 +285,9 @@ fn main() { let num_nodes = 9; // 1 main + 8 peers let mut nodes: Vec> = Vec::with_capacity(num_nodes); let mut node_ids: Vec = Vec::with_capacity(num_nodes); - let mut addrs: Vec = Vec::with_capacity(num_nodes); for i in 0..num_nodes { - let addr: SocketAddr = format!("127.0.0.1:{}", 7000 + i).parse().unwrap(); let config = DistributedNodeConfig { - listen_addr: addr, swim: swim_config.clone(), cache_capacity: if i == 0 { 1000 } else { 100 }, republish_interval: 500, @@ -296,37 +295,17 @@ fn main() { }; let node = DistributedNode::new(config); node_ids.push(node.node_id()); - addrs.push(addr); nodes.push(Some(node)); } // Join handshakes: nodes[1..] join via seed (node 0). - let seed_addr = addrs[0]; for i in 1..num_nodes { - let join_actions = nodes[i].as_ref().unwrap().join(&[seed_addr]); - let tagged_responses = deliver_actions_tagged( - &join_actions, - node_ids[i], - addrs[i], - &mut nodes, - &node_ids, - &addrs, - ); - for (responder_idx, response_actions) in tagged_responses { - deliver_actions_tagged( - &response_actions, - node_ids[responder_idx], - addrs[responder_idx], - &mut nodes, - &node_ids, - &addrs, - ); - } + simulate_join(i, 0, &mut nodes, &node_ids); } // Settle: let SWIM converge initial membership. for _ in 0..5 { - tick_all_and_deliver(&mut nodes, &node_ids, &addrs); + tick_all_and_deliver(&mut nodes, &node_ids); } // Register spawned actors in the main node's directory. @@ -387,7 +366,7 @@ fn main() { } // Tick all distribution nodes and deliver SWIM actions - tick_all_and_deliver(&mut nodes, &node_ids, &addrs); + tick_all_and_deliver(&mut nodes, &node_ids); // Periodically resolve actors from main node if round % 50 == 25 { @@ -446,35 +425,15 @@ fn main() { // Revive peer 8 (new node + rejoin) if churn_pos == 150 { let config = DistributedNodeConfig { - listen_addr: addrs[8], swim: swim_config.clone(), cache_capacity: 100, republish_interval: 500, ..Default::default() }; let revived = DistributedNode::new(config); - let join_actions = revived.join(&[seed_addr]); nodes[8] = Some(revived); node_ids[8] = nodes[8].as_ref().unwrap().node_id(); - - let tagged_responses = deliver_actions_tagged( - &join_actions, - node_ids[8], - addrs[8], - &mut nodes, - &node_ids, - &addrs, - ); - for (responder_idx, response_actions) in tagged_responses { - deliver_actions_tagged( - &response_actions, - node_ids[responder_idx], - addrs[responder_idx], - &mut nodes, - &node_ids, - &addrs, - ); - } + simulate_join(8, 0, &mut nodes, &node_ids); tracing::info!("revived peer 8 (rejoined cluster)"); } @@ -488,19 +447,15 @@ fn main() { let tagged_responses = deliver_actions_tagged( &leave_actions, node_ids[7], - addrs[7], &mut nodes, &node_ids, - &addrs, ); for (responder_idx, response_actions) in tagged_responses { deliver_actions_tagged( &response_actions, node_ids[responder_idx], - addrs[responder_idx], &mut nodes, &node_ids, - &addrs, ); } } @@ -511,35 +466,15 @@ fn main() { // Rejoin peer 7 if churn_pos == 350 { let config = DistributedNodeConfig { - listen_addr: addrs[7], swim: swim_config.clone(), cache_capacity: 100, republish_interval: 500, ..Default::default() }; let revived = DistributedNode::new(config); - let join_actions = revived.join(&[seed_addr]); nodes[7] = Some(revived); node_ids[7] = nodes[7].as_ref().unwrap().node_id(); - - let tagged_responses = deliver_actions_tagged( - &join_actions, - node_ids[7], - addrs[7], - &mut nodes, - &node_ids, - &addrs, - ); - for (responder_idx, response_actions) in tagged_responses { - deliver_actions_tagged( - &response_actions, - node_ids[responder_idx], - addrs[responder_idx], - &mut nodes, - &node_ids, - &addrs, - ); - } + simulate_join(7, 0, &mut nodes, &node_ids); tracing::info!("peer 7 rejoined the cluster"); } } diff --git a/crates/runtime-dashboard/src/bin/swactor-node.rs b/crates/runtime-dashboard/src/bin/swactor-node.rs index 50e0300..6737f5a 100644 --- a/crates/runtime-dashboard/src/bin/swactor-node.rs +++ b/crates/runtime-dashboard/src/bin/swactor-node.rs @@ -1,4 +1,3 @@ -use std::net::SocketAddr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; @@ -10,8 +9,8 @@ use swactor::actor::{ActorInterface, Ctx}; use swactor::config::RuntimeConfig; use swactor::runtime::Runtime; -use distribution::driver::NodeDriver; use distribution::node::DistributedNodeConfig; +use distribution::registry::RegistryConfig; use distribution::snapshot::DistributionNodeSnapshot; use distribution::swim::probe::SwimConfig; @@ -24,13 +23,21 @@ use runtime_dashboard::{start_dashboard, DashboardConfig}; #[derive(Parser)] #[command(name = "swactor-node", about = "Swactor distributed node")] struct Args { - /// Address to listen on for SWIM protocol (e.g. 10.0.1.10:7000) - #[arg(long)] - listen: SocketAddr, + /// Transport to use: tcp or iroh + #[arg(long, default_value = "tcp")] + transport: String, - /// Seed node address to join (omit for the seed node itself) + /// Address to listen on for TCP transport (e.g. 10.0.1.10:7000) #[arg(long)] - seed: Option, + listen: Option, + + /// Seed node address to join (TCP mode: host:port) + #[arg(long)] + seed: Option, + + /// Seed node's iroh public key (iroh mode: hex-encoded 32-byte key) + #[arg(long)] + seed_node_id: Option, /// Dashboard HTTP port #[arg(long, default_value = "9090")] @@ -103,7 +110,7 @@ fn main() { let handle = rt.run().expect("failed to start runtime"); dash.set_runtime(handle.runtime.clone(), collector); - // Create distribution node driver + // Distribution config (shared between transports) let swim_config = SwimConfig { probe_interval: 5, probe_timeout: 3, @@ -112,41 +119,64 @@ fn main() { dead_reprobe_interval: 50, }; let node_config = DistributedNodeConfig { - listen_addr: args.listen, swim: swim_config, cache_capacity: 1000, republish_interval: 500, ..Default::default() }; - let mut driver = NodeDriver::new(node_config).expect("failed to create node driver"); + + match args.transport.as_str() { + #[cfg(feature = "tcp")] + "tcp" => run_tcp(args, node_config, &handle, &dash, &stop), + #[cfg(feature = "iroh")] + "iroh" => run_iroh(args, node_config, &handle, &dash, &stop), + other => { + eprintln!("Unknown or unavailable transport: {other}"); + eprintln!("Available transports:"); + #[cfg(feature = "tcp")] + eprintln!(" tcp"); + #[cfg(feature = "iroh")] + eprintln!(" iroh"); + std::process::exit(1); + } + } + + eprintln!("\nShutting down..."); + handle.shutdown(); + dash.shutdown(); + handle.join(); +} + +// ── TCP transport ──────────────────────────────────────────────────────── + +#[cfg(feature = "tcp")] +fn run_tcp( + args: Args, + node_config: DistributedNodeConfig, + handle: &swactor::runtime::RuntimeHandle, + dash: &runtime_dashboard::DashboardHandle, + stop: &Arc, +) { + use distribution::driver::NodeDriver; + + let listen_addr = args.listen.expect("--listen is required for TCP mode"); + let mut driver = NodeDriver::new(listen_addr, node_config).expect("failed to create node driver"); eprintln!( - "Node {} listening on {}", + "Node {} listening on {} (TCP)", hex(&driver.node_id().0[..4]), driver.listen_addr(), ); // Join seed if provided if let Some(seed) = args.seed { - eprintln!("Joining cluster via seed {seed}"); - driver.join(&[seed]); + let seed_addr: std::net::SocketAddr = seed.parse().expect("invalid seed address"); + eprintln!("Joining cluster via seed {seed_addr}"); + driver.join(&[seed_addr]); } // Spawn and register actors - let mut actor_addrs = Vec::new(); - for _ in 0..args.actors { - match handle.runtime.spawn(HeartbeatActor) { - Ok(addr) => { - driver.node_mut().register_actor(addr, 1); - actor_addrs.push(addr); - } - Err(e) => eprintln!("failed to spawn actor: {e}"), - } - } - - if !actor_addrs.is_empty() { - eprintln!("Registered {} actors", actor_addrs.len()); - } + let actor_addrs = spawn_actors(args.actors, handle, driver.node_mut()); // Wire distribution snapshot to dashboard let cached_snapshot: Arc>> = @@ -156,33 +186,119 @@ fn main() { }; dash.set_distribution(Arc::new(provider)); - eprintln!( - "Dashboard at http://0.0.0.0:{}", - args.dashboard_port - ); + eprintln!("Dashboard at http://0.0.0.0:{}", args.dashboard_port); // Main loop while !stop.load(Ordering::Relaxed) { driver.recv(); driver.tick(); - // Send heartbeats to keep actors alive for addr in &actor_addrs { let _ = handle.runtime.send_to(*addr, Heartbeat); } - // Update dashboard snapshot *cached_snapshot.lock().unwrap() = Some(driver.snapshot()); + thread::sleep(Duration::from_millis(100)); + } +} +// ── iroh transport ─────────────────────────────────────────────────────── + +#[cfg(feature = "iroh")] +fn run_iroh( + args: Args, + node_config: DistributedNodeConfig, + handle: &swactor::runtime::RuntimeHandle, + dash: &runtime_dashboard::DashboardHandle, + stop: &Arc, +) { + use distribution::iroh_driver::{IrohDriver, IrohDriverConfig}; + use iroh::RelayMode; + + let iroh_config = IrohDriverConfig { + secret_key: None, + relay_mode: RelayMode::Default, + node: node_config, + }; + let mut driver = IrohDriver::new(iroh_config).expect("failed to create iroh driver"); + + eprintln!( + "Node {} started (iroh)", + hex(&driver.node_id().0[..4]), + ); + + // Join seed if provided + if let Some(seed_hex) = args.seed_node_id { + let seed_bytes = hex_to_bytes(&seed_hex).expect("invalid seed node ID hex"); + let seed_key = iroh::PublicKey::from_bytes(&seed_bytes).expect("invalid seed public key"); + eprintln!("Joining cluster via seed {}", &seed_hex[..8]); + driver.join(&[seed_key]); + } + + // Spawn and register actors + let actor_addrs = spawn_actors(args.actors, handle, driver.node_mut()); + + // Wire distribution snapshot to dashboard + let cached_snapshot: Arc>> = + Arc::new(Mutex::new(Some(driver.snapshot()))); + let provider = SnapshotProvider { + snapshot: Arc::clone(&cached_snapshot), + }; + dash.set_distribution(Arc::new(provider)); + + eprintln!("Dashboard at http://0.0.0.0:{}", args.dashboard_port); + + // Main loop + while !stop.load(Ordering::Relaxed) { + driver.recv(); + driver.tick(); + + for addr in &actor_addrs { + let _ = handle.runtime.send_to(*addr, Heartbeat); + } + + *cached_snapshot.lock().unwrap() = Some(driver.snapshot()); thread::sleep(Duration::from_millis(100)); } - eprintln!("\nShutting down..."); - handle.shutdown(); - dash.shutdown(); - handle.join(); + driver.shutdown(); +} + +// ── Helpers ────────────────────────────────────────────────────────────── + +fn spawn_actors( + count: usize, + handle: &swactor::runtime::RuntimeHandle, + node: &mut distribution::node::DistributedNode, +) -> Vec { + let mut addrs = Vec::new(); + for _ in 0..count { + match handle.runtime.spawn(HeartbeatActor) { + Ok(addr) => { + node.register_actor(addr, 1); + addrs.push(addr); + } + Err(e) => eprintln!("failed to spawn actor: {e}"), + } + } + if !addrs.is_empty() { + eprintln!("Registered {} actors", addrs.len()); + } + addrs } fn hex(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{b:02x}")).collect() } + +#[cfg(feature = "iroh")] +fn hex_to_bytes(hex: &str) -> Option<[u8; 32]> { + if hex.len() != 64 { + return None; + } + let mut bytes = [0u8; 32]; + for i in 0..32 { + bytes[i] = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).ok()?; + } + Some(bytes) +} diff --git a/crates/simulation/src/distribution/sim.rs b/crates/simulation/src/distribution/sim.rs index eb084ee..3de07df 100644 --- a/crates/simulation/src/distribution/sim.rs +++ b/crates/simulation/src/distribution/sim.rs @@ -1,5 +1,4 @@ use std::collections::HashSet; -use std::net::SocketAddr; use distribution::node::{DistributedNode, DistributedNodeConfig, ResolveResult}; use distribution::swim::node::NodeAction; @@ -182,15 +181,12 @@ fn run_simulation_inner(config: DistributionSimConfig) -> (DistTrace, Vec