From 05f62fc22e5be73acecaae0f460a51ba07575b48 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 19:00:17 -0300 Subject: [PATCH 01/29] Initial implementation - Test `bundle_output` --- Cargo.lock | 380 +++++++++++++++++++++++----------------- Cargo.toml | 2 + crates/app/Cargo.toml | 3 + crates/app/src/lib.rs | 3 + crates/app/src/utils.rs | 249 ++++++++++++++++++++++++++ 5 files changed, 478 insertions(+), 159 deletions(-) create mode 100644 crates/app/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index aace3e87..43f6f902 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -439,7 +445,7 @@ checksum = "ce8849c74c9ca0f5a03da1c865e3eb6f768df816e67dd3721a398a8a7e398011" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -562,7 +568,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -580,7 +586,7 @@ dependencies = [ "proc-macro2", "quote", "sha3", - "syn 2.0.116", + "syn 2.0.117", "syn-solidity", ] @@ -598,7 +604,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.116", + "syn 2.0.117", "syn-solidity", ] @@ -689,7 +695,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -759,9 +765,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "ark-ff" @@ -848,7 +854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -886,7 +892,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -991,7 +997,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -1003,7 +1009,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1087,7 +1093,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1098,7 +1104,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1140,7 +1146,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1282,7 +1288,7 @@ checksum = "7b9a5040dce49a7642c97ccb1ae59567098967b5d52c29773f1299a42d23bb39" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1475,7 +1481,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1498,7 +1504,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1523,9 +1529,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byte-slice-cast" @@ -1550,9 +1556,9 @@ dependencies = [ [[package]] name = "c-kzg" -version = "2.1.5" +version = "2.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" +checksum = "1a0f582957c24870b7bfd12bf562c40b4734b533cafbaf8ded31d6d85f462c01" dependencies = [ "blst", "cc", @@ -1652,9 +1658,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -1704,9 +1710,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.59" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -1714,9 +1720,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.59" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -1733,7 +1739,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -1784,9 +1790,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" +checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" dependencies = [ "cfg-if", "cpufeatures", @@ -1894,6 +1900,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" version = "0.8.2" @@ -2027,7 +2042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2074,7 +2089,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2118,7 +2133,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2133,7 +2148,7 @@ dependencies = [ "quote", "serde", "strsim", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2146,7 +2161,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2157,7 +2172,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2168,7 +2183,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2179,7 +2194,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2219,7 +2234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2267,9 +2282,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -2305,7 +2320,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.116", + "syn 2.0.117", "unicode-xid", ] @@ -2338,7 +2353,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2418,7 +2433,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2478,7 +2493,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2498,7 +2513,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -2688,6 +2703,16 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2818,7 +2843,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3585,7 +3610,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -3721,9 +3746,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -4328,7 +4353,7 @@ checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" dependencies = [ "heck", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4507,25 +4532,26 @@ dependencies = [ "thiserror 2.0.18", "tracing", "yamux 0.12.1", - "yamux 0.13.8", + "yamux 0.13.9", ] [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags 2.11.0", "libc", - "redox_syscall 0.7.1", + "plain", + "redox_syscall 0.7.3", ] [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" dependencies = [ "cc", "libc", @@ -4535,9 +4561,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -4593,7 +4619,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4604,7 +4630,7 @@ checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -4660,6 +4686,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -4673,9 +4709,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.13" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -4984,7 +5020,7 @@ checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5044,7 +5080,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_with", - "syn 2.0.116", + "syn 2.0.117", "thiserror 2.0.18", "uuid", "validator", @@ -5110,7 +5146,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5178,7 +5214,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5232,7 +5268,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5310,29 +5346,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -5356,6 +5392,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plotters" version = "0.3.7" @@ -5392,6 +5434,7 @@ dependencies = [ "backon", "bon", "chrono", + "flate2", "hex", "k256", "pluto-build-proto", @@ -5407,6 +5450,8 @@ dependencies = [ "reqwest 0.13.2", "serde", "serde_json", + "tar", + "tempfile", "thiserror 2.0.18", "tokio", "tokio-util", @@ -5795,7 +5840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5846,7 +5891,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5878,7 +5923,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5935,7 +5980,7 @@ dependencies = [ "prost 0.14.3", "prost-types 0.14.3", "regex", - "syn 2.0.116", + "syn 2.0.117", "tempfile", ] @@ -5949,7 +5994,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -5962,7 +6007,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6013,9 +6058,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.1" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd58c6a1fc307e1092aa0bb23d204ca4d1f021764142cd0424dccc84d2d5d106" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "memchr", "serde", @@ -6213,9 +6258,9 @@ dependencies = [ [[package]] name = "rapidhash" -version = "4.4.0" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111325c42c4bafae99e777cd77b40dea9a2b30c69e9d8c74b6eccd7fba4337de" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" dependencies = [ "rustversion", ] @@ -6264,9 +6309,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ "bitflags 2.11.0", ] @@ -6288,7 +6333,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6316,9 +6361,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -6539,9 +6584,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags 2.11.0", "errno", @@ -6552,9 +6597,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -6778,9 +6823,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.0", "core-foundation 0.10.1", @@ -6791,9 +6836,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -6869,7 +6914,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6905,7 +6950,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -6931,9 +6976,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "base64 0.22.1", "chrono", @@ -6950,14 +6995,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7060,6 +7105,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "slab" version = "0.4.12" @@ -7170,7 +7221,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7181,7 +7232,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7202,7 +7253,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7224,9 +7275,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -7242,7 +7293,7 @@ dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7262,7 +7313,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7323,11 +7374,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", "getrandom 0.4.1", @@ -7354,7 +7416,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7365,7 +7427,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "test-case-core", ] @@ -7425,7 +7487,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7436,7 +7498,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7542,13 +7604,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7669,9 +7731,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tonic" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f32a6f80051a4111560201420c7885d0082ba9efe2ab61875c587bb6b18b9a0" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" dependencies = [ "async-trait", "axum", @@ -7698,9 +7760,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f86539c0089bfd09b1f8c0ab0239d80392af74c21bc9e0f15e1b4aca4c1647f" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" dependencies = [ "bytes", "prost 0.14.3", @@ -7776,7 +7838,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -7871,7 +7933,7 @@ dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8090,7 +8152,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8145,7 +8207,7 @@ source = "git+https://github.com/matter-labs/vise?rev=73c654303d8190023cf30034d6 dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8208,9 +8270,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -8221,9 +8283,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", "futures-util", @@ -8235,9 +8297,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8245,22 +8307,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -8328,9 +8390,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -8473,7 +8535,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8484,7 +8546,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8495,7 +8557,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8506,7 +8568,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -8913,7 +8975,7 @@ dependencies = [ "heck", "indexmap 2.13.0", "prettyplease", - "syn 2.0.116", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -8929,7 +8991,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -9057,9 +9119,9 @@ dependencies = [ [[package]] name = "yamux" -version = "0.13.8" +version = "0.13.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deab71f2e20691b4728b349c6cee8fc7223880fa67b6b4f92225ec32225447e5" +checksum = "c650efd29044140aa63caaf80129996a9e2659a2ab7045a7e061807d02fc8549" dependencies = [ "futures", "log", @@ -9099,28 +9161,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -9140,7 +9202,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", "synstructure", ] @@ -9161,7 +9223,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] @@ -9194,7 +9256,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.116", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 34b514cc..29e2fc53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,8 @@ testcontainers = "0.26" test-case = "3.3" tree_hash = "0.12" tree_hash_derive = "0.12" +tar = "0.4" +flate2 = "1.1" wiremock = "0.6" # Crates in the workspace diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 2d1ee771..f55bd2d6 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -28,6 +28,9 @@ serde_json.workspace = true hex.workspace = true k256.workspace = true bon.workspace = true +flate2.workspace = true +tar.workspace = true +tempfile.workspace = true pluto-cluster.workspace = true pluto-k1util.workspace = true pluto-crypto.workspace = true diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index 3252fe67..9c564367 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -24,3 +24,6 @@ pub mod obolapi; /// Ethereum CL RPC client management. pub mod eth2wrap; + +/// Utility helpers for archiving, extracting, and comparing files/directories. +pub mod utils; diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs new file mode 100644 index 00000000..b0cbb6b9 --- /dev/null +++ b/crates/app/src/utils.rs @@ -0,0 +1,249 @@ +use std::{fs, io, path}; + +/// Error type for util operations. +#[derive(Debug, thiserror::Error)] +pub enum UtilsError { + /// Underlying IO error occurred. + #[error("IO error: {0}")] + IOError(#[from] io::Error), + + /// File exceeds the maximum allowed size during extraction. + #[error("File too large: {0}")] + FileTooLarge(String), + + /// Directories have different number of entries. + #[error("Directory entry count mismatch: {0} vs {1}")] + DirectoryEntryCountMismatch(usize, usize), + + /// Unexpected file name. + #[error("File name mismatch: expected {0}, found {1}")] + FileNameMismatch(String, String), + + /// Unexpected file contents. + #[error("File content mismatch: expected {0}, found {1}")] + FileContentMismatch(String, String), + + /// One entry is a file and the other is a directory. + + #[error("Type mismatch: expected {0}, found {1}")] + TypeMismatch(String, String), +} + +type Result = std::result::Result; + +/// Archives `target_path` into a gzipped tarball named `filename` in +/// `target_path`. After successfully creating the archive, it deletes the +/// original files from disk. +pub fn bundle_output( + target_path: impl AsRef, + filename: impl AsRef, +) -> Result<()> { + // Create output file + let tar_file = tempfile::NamedTempFile::new()?; + let tar_file_path = tar_file.path().to_owned(); + + // Compress and encode + let encoder = flate2::write::GzEncoder::new(tar_file, flate2::Compression::default()); + let mut tar = tar::Builder::new(encoder); + tar.append_dir_all("", &target_path)?; + tar.finish()?; + + // Delete all files from the `target_dir` + fs::remove_dir_all(&target_path)?; + fs::create_dir_all(&target_path)?; + + // Move the created tarball to the target location + let output_path = path::Path::new(target_path.as_ref()).join(filename.as_ref()); + fs::rename(tar_file_path, output_path)?; + + Ok(()) +} + +/// Extracts a `.tar.gz` archive to the target path. +pub fn extract_archive( + archive_path: impl AsRef, + target_path: impl AsRef, +) -> Result<()> { + // Create the decompressor. + let tar_gz = fs::File::open(archive_path)?; + let decompressor = flate2::read::GzDecoder::new(tar_gz); + let mut archive = tar::Archive::new(decompressor); + + // Extract each file, verifying that it does not exceed a reasonable size limit + // to prevent DoS attacks. + const MAX_FILE: u64 = 100 * 1024 * 1024; // 100MB limit per file + for entry in archive.entries()? { + let mut entry = entry?; + if entry.size() > MAX_FILE { + return Err(UtilsError::FileTooLarge( + entry.path()?.display().to_string(), + )); + } + entry.unpack_in(&target_path)?; + } + + Ok(()) +} + +/// Recursively compares two directories and their contents. +pub fn compare_directories( + dir1: impl AsRef, + dir2: impl AsRef, +) -> io::Result<()> { + let mut entries1 = fs::read_dir(dir1)?.collect::, _>>()?; + let mut entries2 = fs::read_dir(dir2)?.collect::, _>>()?; + + entries1.sort_by_key(|e| e.file_name()); + entries2.sort_by_key(|e| e.file_name()); + + if entries1.len() != entries2.len() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!( + "Directory entry count mismatch: {} vs {}", + entries1.len(), + entries2.len() + ), + )); + } + + for (entry1, entry2) in entries1.iter().zip(entries2.iter()) { + let path1 = entry1.path(); + let path2 = entry2.path(); + + if path1.is_dir() && path2.is_dir() { + compare_directories(&path1, &path2)?; + } else if path1.is_file() && path2.is_file() { + let name1 = entry1.file_name(); + let name2 = entry2.file_name(); + if name1 != name2 { + return Err(io::Error::new( + io::ErrorKind::Other, + format!( + "File name mismatch: expected {}, found {}", + name1.to_string_lossy(), + name2.to_string_lossy() + ), + )); + } + + let content1 = fs::read(&path1)?; + let content2 = fs::read(&path2)?; + if content1 != content2 { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Files {} and {} differ", path1.display(), path2.display()), + )); + } + } else { + return Err(io::Error::new( + io::ErrorKind::Other, + format!( + "One is a file and the other is a directory: {} and {}", + path1.display(), + path2.display() + ), + )); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, fs, io, path}; + + #[test] + fn bundle_output() { + // Create a temporary directory for testing + let test_dir = tempfile::tempdir().unwrap(); + + // Create a complex file tree structure + let test_files = HashMap::from([ + ("root_file.txt", "This is a root file content".as_bytes()), + ( + "nested/level1.json", + r#"{"key": "value", "number": 42}"#.as_bytes(), + ), + ( + "nested/deep/level2.md", + "# Deep Nested File\n\nThis is markdown content.".as_bytes(), + ), + ( + "nested/deep/deeper/level3.yaml", + "key: value\nlist:\n - item1\n - item2".as_bytes(), + ), + ( + "validator_keys/keystore-1.json", + r#"{"crypto": {"cipher": "test"}, "pubkey": "0x123"}"#.as_bytes(), + ), + ( + "validator_keys/keystore-2.json", + r#"{"crypto": {"cipher": "test"}, "pubkey": "0x456"}"#.as_bytes(), + ), + ( + "cluster-lock.json", + r#"{"lock_hash": "0xabc", "definition": {}}"#.as_bytes(), + ), + ( + "deposit_data.json", + r#"[{"pubkey": "0x123", "amount": 32000000000}]"#.as_bytes(), + ), + ("empty_dir/placeholder.txt", b""), + ("binary_file.bin", b"\x00\x01\x02\x03\xFF\xFE\xFD"), + ( + "special_chars_äöü.txt", + "File with special characters: äöüß".as_bytes(), + ), + ]); + + // Create all test files and directories + for (rel_path, content) in &test_files { + let full_path = test_dir.path().join(rel_path); + fs::create_dir_all(full_path.parent().unwrap()).unwrap(); + fs::write(full_path, content).unwrap(); + } + + // Create a backup of the original structure for comparison + let backup_dir = tempfile::tempdir().unwrap(); + copy_dir_all(test_dir.path(), backup_dir.path()).unwrap(); + + // Call `bundle_output` to create the tar.gz archive + let archive_name = "test_bundle.tar.gz"; + super::bundle_output(test_dir.path(), archive_name).unwrap(); + + // Verify that the archive file exists + let archive_path = test_dir.path().join(archive_name); + assert!(archive_path.exists(), "Archive file should exist"); + + // Verify that original files are deleted (except the archive) + let entries: Vec<_> = fs::read_dir(test_dir.path()).unwrap().collect(); + assert!(entries.len() == 1, "Only the archive file should remain"); + let actual_archive_name = entries[0].as_ref().unwrap().file_name(); + assert_eq!(actual_archive_name, archive_name); + + // Extract the archive to a new directory + let extract_dir = tempfile::tempdir().unwrap(); + super::extract_archive(archive_path, extract_dir.path()).unwrap(); + + // Compare the extracted content with the original backup + super::compare_directories(backup_dir, extract_dir) + .expect("Extracted directory should match original structure"); + } + + /// Recursively copies all files and directories from `from` to `to`. + fn copy_dir_all(from: impl AsRef, to: impl AsRef) -> io::Result<()> { + fs::create_dir_all(&to)?; // Create the destination directory and all its parents + for entry in fs::read_dir(from)? { + let entry = entry?; + let file_type = entry.file_type()?; + if file_type.is_dir() { + copy_dir_all(entry.path(), to.as_ref().join(entry.file_name()))?; + } else { + fs::copy(entry.path(), to.as_ref().join(entry.file_name()))?; // Copy the file + } + } + Ok(()) + } +} From b10bff1e13bece4d0b52bef5f5aafb5fb1d7fde2 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 20:33:34 -0300 Subject: [PATCH 02/29] Add `hex7` --- Cargo.lock | 1 + crates/app/Cargo.toml | 1 + crates/app/src/utils.rs | 15 +++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 43f6f902..6bf12011 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5452,6 +5452,7 @@ dependencies = [ "serde_json", "tar", "tempfile", + "test-case", "thiserror 2.0.18", "tokio", "tokio-util", diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index f55bd2d6..d799fd7f 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -40,6 +40,7 @@ pluto-build-proto.workspace = true [dev-dependencies] wiremock.workspace = true +test-case.workspace = true [lints] workspace = true diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index b0cbb6b9..24bb74cc 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -31,6 +31,12 @@ pub enum UtilsError { type Result = std::result::Result; +/// Returns the first 7 (or less) hex chars of the provided bytes. +pub fn hex_7(input: &[u8]) -> String { + let as_string = hex::encode(input); + as_string.chars().take(7).collect() +} + /// Archives `target_path` into a gzipped tarball named `filename` in /// `target_path`. After successfully creating the archive, it deletes the /// original files from disk. @@ -153,6 +159,15 @@ pub fn compare_directories( #[cfg(test)] mod tests { use std::{collections::HashMap, fs, io, path}; + use test_case::test_case; + + #[test_case(&hex::decode("433287d255abf237992d2279af5b1a1bb2c3d7124c97906edd848ebbb541a1c7").unwrap(), "433287d")] + #[test_case("aaa".as_bytes(), "616161")] + #[test_case("".as_bytes(), "")] + fn hex_7(bytes: &[u8], expected: &str) { + let actual = super::hex_7(bytes); + assert_eq!(actual, expected); + } #[test] fn bundle_output() { From a8f378b40a0a139ef0c2a283207dca20e8a37e39 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 20:38:32 -0300 Subject: [PATCH 03/29] Add `compare_directories_identical` --- crates/app/src/utils.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 24bb74cc..a97a7c39 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -247,6 +247,33 @@ mod tests { .expect("Extracted directory should match original structure"); } + #[test] + fn compare_directories_identical() { + let dir1 = tempfile::tempdir().unwrap(); + let test_files = HashMap::from([ + ("file1.txt", "content1".as_bytes()), + ("nested/file2.json", r#"{"key": "value"}"#.as_bytes()), + ("nested/deep/file3.md", "# Header\nContent".as_bytes()), + ("binary.bin", b"\x00\x01\x02\x03"), + ( + "special_chars_äöü.txt", + "Special characters: äöüß".as_bytes(), + ), + ]); + for (rel_path, content) in test_files { + let full_path = dir1.path().join(rel_path); + fs::create_dir_all(full_path.parent().unwrap()).unwrap(); + fs::write(full_path, content).unwrap(); + } + + let dir2 = tempfile::tempdir().unwrap(); + copy_dir_all(dir1.path(), dir2.path()).unwrap(); + + let result = super::compare_directories(dir1, dir2); + + assert!(result.is_ok()); + } + /// Recursively copies all files and directories from `from` to `to`. fn copy_dir_all(from: impl AsRef, to: impl AsRef) -> io::Result<()> { fs::create_dir_all(&to)?; // Create the destination directory and all its parents From 1635da163fcc91f2dfc46d947af85d51d651237f Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 20:49:20 -0300 Subject: [PATCH 04/29] Add `compare_directories_missing_file` - Fix an issue where temp files where being dropped earlier --- crates/app/src/utils.rs | 59 ++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index a97a7c39..05b3b813 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -95,7 +95,7 @@ pub fn extract_archive( pub fn compare_directories( dir1: impl AsRef, dir2: impl AsRef, -) -> io::Result<()> { +) -> Result<()> { let mut entries1 = fs::read_dir(dir1)?.collect::, _>>()?; let mut entries2 = fs::read_dir(dir2)?.collect::, _>>()?; @@ -103,13 +103,9 @@ pub fn compare_directories( entries2.sort_by_key(|e| e.file_name()); if entries1.len() != entries2.len() { - return Err(io::Error::new( - io::ErrorKind::Other, - format!( - "Directory entry count mismatch: {} vs {}", - entries1.len(), - entries2.len() - ), + return Err(UtilsError::DirectoryEntryCountMismatch( + entries1.len(), + entries2.len(), )); } @@ -123,32 +119,24 @@ pub fn compare_directories( let name1 = entry1.file_name(); let name2 = entry2.file_name(); if name1 != name2 { - return Err(io::Error::new( - io::ErrorKind::Other, - format!( - "File name mismatch: expected {}, found {}", - name1.to_string_lossy(), - name2.to_string_lossy() - ), + return Err(UtilsError::FileNameMismatch( + name1.to_string_lossy().to_string(), + name2.to_string_lossy().to_string(), )); } let content1 = fs::read(&path1)?; let content2 = fs::read(&path2)?; if content1 != content2 { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("Files {} and {} differ", path1.display(), path2.display()), + return Err(UtilsError::FileContentMismatch( + path1.display().to_string(), + path2.display().to_string(), )); } } else { - return Err(io::Error::new( - io::ErrorKind::Other, - format!( - "One is a file and the other is a directory: {} and {}", - path1.display(), - path2.display() - ), + return Err(UtilsError::TypeMismatch( + path1.display().to_string(), + path2.display().to_string(), )); } } @@ -243,7 +231,7 @@ mod tests { super::extract_archive(archive_path, extract_dir.path()).unwrap(); // Compare the extracted content with the original backup - super::compare_directories(backup_dir, extract_dir) + super::compare_directories(backup_dir.path(), extract_dir.path()) .expect("Extracted directory should match original structure"); } @@ -269,11 +257,28 @@ mod tests { let dir2 = tempfile::tempdir().unwrap(); copy_dir_all(dir1.path(), dir2.path()).unwrap(); - let result = super::compare_directories(dir1, dir2); + let result = super::compare_directories(dir1.path(), dir2.path()); assert!(result.is_ok()); } + #[test] + fn compare_directories_missing_file() { + let dir1 = tempfile::tempdir().unwrap(); + let some_file_path = dir1.path().join("file.txt"); + fs::create_dir_all(some_file_path.parent().unwrap()).unwrap(); + fs::write(some_file_path, b"content").unwrap(); + + let dir2 = tempfile::tempdir().unwrap(); + + let result = super::compare_directories(dir1.path(), dir2.path()); + + assert!(matches!( + result, + Err(super::UtilsError::DirectoryEntryCountMismatch(1, 0)) + )); + } + /// Recursively copies all files and directories from `from` to `to`. fn copy_dir_all(from: impl AsRef, to: impl AsRef) -> io::Result<()> { fs::create_dir_all(&to)?; // Create the destination directory and all its parents From 6b65e66dc1dac38437b28f3e649859e1289725b0 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 21:07:22 -0300 Subject: [PATCH 05/29] Add `compare_directories_different_content` - Revamp error cases --- crates/app/src/utils.rs | 63 +++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 05b3b813..dbc69b73 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -9,24 +9,23 @@ pub enum UtilsError { /// File exceeds the maximum allowed size during extraction. #[error("File too large: {0}")] - FileTooLarge(String), + FileTooLarge(path::PathBuf), /// Directories have different number of entries. - #[error("Directory entry count mismatch: {0} vs {1}")] + #[error("Directory entry count mismatch: expected {0}, found {1}")] DirectoryEntryCountMismatch(usize, usize), - /// Unexpected file name. - #[error("File name mismatch: expected {0}, found {1}")] - FileNameMismatch(String, String), - /// Unexpected file contents. - #[error("File content mismatch: expected {0}, found {1}")] - FileContentMismatch(String, String), + #[error("File content mismatch: {0} vs {1}")] + FileContentMismatch(path::PathBuf, path::PathBuf), - /// One entry is a file and the other is a directory. + /// Unexpected path. + #[error("File name mismatch: expected {0}, found {1}")] + FileNameMismatch(String, String), - #[error("Type mismatch: expected {0}, found {1}")] - TypeMismatch(String, String), + /// One entry is a file and the other is a directory for a given path. + #[error("Type mismatch: {0} vs {1}")] + PathTypeMismatch(path::PathBuf, path::PathBuf), } type Result = std::result::Result; @@ -81,9 +80,7 @@ pub fn extract_archive( for entry in archive.entries()? { let mut entry = entry?; if entry.size() > MAX_FILE { - return Err(UtilsError::FileTooLarge( - entry.path()?.display().to_string(), - )); + return Err(UtilsError::FileTooLarge(entry.path()?.to_path_buf())); } entry.unpack_in(&target_path)?; } @@ -120,24 +117,18 @@ pub fn compare_directories( let name2 = entry2.file_name(); if name1 != name2 { return Err(UtilsError::FileNameMismatch( - name1.to_string_lossy().to_string(), - name2.to_string_lossy().to_string(), + name1.display().to_string(), + name2.display().to_string(), )); } let content1 = fs::read(&path1)?; let content2 = fs::read(&path2)?; if content1 != content2 { - return Err(UtilsError::FileContentMismatch( - path1.display().to_string(), - path2.display().to_string(), - )); + return Err(UtilsError::FileContentMismatch(path1, path2)); } } else { - return Err(UtilsError::TypeMismatch( - path1.display().to_string(), - path2.display().to_string(), - )); + return Err(UtilsError::PathTypeMismatch(path1, path2)); } } @@ -279,6 +270,30 @@ mod tests { )); } + #[test] + fn compare_directories_different_content() { + let dir1 = tempfile::tempdir().unwrap(); + { + let some_file_path = dir1.path().join("file.txt"); + fs::create_dir_all(some_file_path.parent().unwrap()).unwrap(); + fs::write(some_file_path, b"content1").unwrap(); + } + + let dir2 = tempfile::tempdir().unwrap(); + { + let some_file_path = dir2.path().join("file.txt"); + fs::create_dir_all(some_file_path.parent().unwrap()).unwrap(); + fs::write(some_file_path, b"content2").unwrap(); + } + + let result = super::compare_directories(dir1.path(), dir2.path()); + + assert!(matches!( + result, + Err(super::UtilsError::FileContentMismatch(_, _)) + )); + } + /// Recursively copies all files and directories from `from` to `to`. fn copy_dir_all(from: impl AsRef, to: impl AsRef) -> io::Result<()> { fs::create_dir_all(&to)?; // Create the destination directory and all its parents From 549be80876fa55d71181db34fc0b2f8feebb6ec1 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 21:08:26 -0300 Subject: [PATCH 06/29] Add `compare_directories_different_sizes` --- crates/app/src/utils.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index dbc69b73..12864196 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -294,6 +294,30 @@ mod tests { )); } + #[test] + fn compare_directories_different_sizes() { + let dir1 = tempfile::tempdir().unwrap(); + { + let some_file_path = dir1.path().join("file.txt"); + fs::create_dir_all(some_file_path.parent().unwrap()).unwrap(); + fs::write(some_file_path, b"short").unwrap(); + } + + let dir2 = tempfile::tempdir().unwrap(); + { + let some_file_path = dir2.path().join("file.txt"); + fs::create_dir_all(some_file_path.parent().unwrap()).unwrap(); + fs::write(some_file_path, b"much longer content").unwrap(); + } + + let result = super::compare_directories(dir1.path(), dir2.path()); + + assert!(matches!( + result, + Err(super::UtilsError::FileContentMismatch(_, _)) + )); + } + /// Recursively copies all files and directories from `from` to `to`. fn copy_dir_all(from: impl AsRef, to: impl AsRef) -> io::Result<()> { fs::create_dir_all(&to)?; // Create the destination directory and all its parents From 30a1a10a4ebaed27e0be20f98c9963b03bcf86b1 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 21:10:15 -0300 Subject: [PATCH 07/29] Add `compare_directories_missing_directory` --- crates/app/src/utils.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 12864196..c5af8422 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -318,6 +318,25 @@ mod tests { )); } + #[test] + fn compare_directories_missing_directory() { + let dir1 = tempfile::tempdir().unwrap(); + { + let some_file_path = dir1.path().join("nested").join("deep").join("file.txt"); + fs::create_dir_all(some_file_path.parent().unwrap()).unwrap(); + fs::write(some_file_path, b"content").unwrap(); + } + + let dir2 = tempfile::tempdir().unwrap(); + + let result = super::compare_directories(dir1.path(), dir2.path()); + + assert!(matches!( + result, + Err(super::UtilsError::DirectoryEntryCountMismatch(_, _)) + )); + } + /// Recursively copies all files and directories from `from` to `to`. fn copy_dir_all(from: impl AsRef, to: impl AsRef) -> io::Result<()> { fs::create_dir_all(&to)?; // Create the destination directory and all its parents From a8eda8842f28d0fafcf3e538acbb63a68b1fb760 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 21:12:18 -0300 Subject: [PATCH 08/29] Add `compare_directories_file_vs_directory` --- crates/app/src/utils.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index c5af8422..3db3af44 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -337,6 +337,29 @@ mod tests { )); } + #[test] + fn compare_directories_file_vs_directory() { + let dir1 = tempfile::tempdir().unwrap(); + { + let some_file_path = dir1.path().join("item"); + fs::create_dir_all(some_file_path.parent().unwrap()).unwrap(); + fs::write(some_file_path, b"content").unwrap(); + } + + let dir2 = tempfile::tempdir().unwrap(); + { + let some_dir_path = dir2.path().join("item"); + fs::create_dir_all(some_dir_path).unwrap(); + } + + let result = super::compare_directories(dir1.path(), dir2.path()); + + assert!(matches!( + result, + Err(super::UtilsError::PathTypeMismatch(_, _)) + )); + } + /// Recursively copies all files and directories from `from` to `to`. fn copy_dir_all(from: impl AsRef, to: impl AsRef) -> io::Result<()> { fs::create_dir_all(&to)?; // Create the destination directory and all its parents From 25625c5748ea9381f82cd9f9ce53f0b5fbffb95e Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 21:13:14 -0300 Subject: [PATCH 09/29] Add `compare_directories_directory_vs_file` --- crates/app/src/utils.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 3db3af44..8d964ab4 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -360,6 +360,29 @@ mod tests { )); } + #[test] + fn compare_directories_directory_vs_file() { + let dir1 = tempfile::tempdir().unwrap(); + { + let some_dir_path = dir1.path().join("item"); + fs::create_dir_all(some_dir_path).unwrap(); + } + + let dir2 = tempfile::tempdir().unwrap(); + { + let some_file_path = dir2.path().join("item"); + fs::create_dir_all(some_file_path.parent().unwrap()).unwrap(); + fs::write(some_file_path, b"content").unwrap(); + } + + let result = super::compare_directories(dir1.path(), dir2.path()); + + assert!(matches!( + result, + Err(super::UtilsError::PathTypeMismatch(_, _)) + )); + } + /// Recursively copies all files and directories from `from` to `to`. fn copy_dir_all(from: impl AsRef, to: impl AsRef) -> io::Result<()> { fs::create_dir_all(&to)?; // Create the destination directory and all its parents From 460b35d00e3df85643e6b7adf6b0f4bff2f65e51 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 21:14:03 -0300 Subject: [PATCH 10/29] Add `compare_directories_empty` --- crates/app/src/utils.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 8d964ab4..2ce0115b 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -383,6 +383,16 @@ mod tests { )); } + #[test] + fn compare_directories_empty() { + let dir1 = tempfile::tempdir().unwrap(); + let dir2 = tempfile::tempdir().unwrap(); + + let result = super::compare_directories(dir1.path(), dir2.path()); + + assert!(result.is_ok(),); + } + /// Recursively copies all files and directories from `from` to `to`. fn copy_dir_all(from: impl AsRef, to: impl AsRef) -> io::Result<()> { fs::create_dir_all(&to)?; // Create the destination directory and all its parents From 255c18986975e567b8bbc932781cefc5c87936e8 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 21:14:33 -0300 Subject: [PATCH 11/29] Add `compare_directories_different_file_names` --- crates/app/src/utils.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 2ce0115b..0f87dbec 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -383,6 +383,30 @@ mod tests { )); } + #[test] + fn compare_directories_different_file_names() { + let dir1 = tempfile::tempdir().unwrap(); + { + let some_file_path = dir1.path().join("file1.txt"); + fs::create_dir_all(some_file_path.parent().unwrap()).unwrap(); + fs::write(some_file_path, b"content").unwrap(); + } + + let dir2 = tempfile::tempdir().unwrap(); + { + let some_file_path = dir2.path().join("file2.txt"); + fs::create_dir_all(some_file_path.parent().unwrap()).unwrap(); + fs::write(some_file_path, b"content").unwrap(); + } + + let result = super::compare_directories(dir1.path(), dir2.path()); + + assert!(matches!( + result, + Err(super::UtilsError::FileNameMismatch(_, _)) + )); + } + #[test] fn compare_directories_empty() { let dir1 = tempfile::tempdir().unwrap(); From 1987fc9f001f351bfd985bd901110b6293cf14b6 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 21:17:19 -0300 Subject: [PATCH 12/29] Add `compare_directories_complex_structure` --- crates/app/src/utils.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 0f87dbec..2620e84b 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -383,6 +383,43 @@ mod tests { )); } + #[test] + fn compare_directories_complex_structure() { + let dir1 = tempfile::tempdir().unwrap(); + let dir2 = tempfile::tempdir().unwrap(); + + let test_files = HashMap::from([ + ("root.txt", "root content".as_bytes()), + ( + "validator_keys/keystore-1.json", + r#"{"crypto": {"cipher": "test"}}"#.as_bytes(), + ), + ( + "validator_keys/keystore-2.json", + r#"{"crypto": {"cipher": "test"}}"#.as_bytes(), + ), + ( + "nested/level1/level2/deep.yaml", + "key: value\narray:\n - item1\n - item2".as_bytes(), + ), + ("cluster-lock.json", r#"{"lock_hash": "0xabc"}"#.as_bytes()), + ("deposit_data.json", r#"[{"pubkey": "0x123"}]"#.as_bytes()), + ("empty_dir/placeholder.txt", b""), + ("binary_data.bin", b"\x00\x01\x02\x03\xFF\xFE\xFD"), + ]); + for (rel_path, content) in test_files { + for dir in [&dir1, &dir2] { + let full_path = dir.path().join(rel_path); + fs::create_dir_all(full_path.parent().unwrap()).unwrap(); + fs::write(full_path, content).unwrap(); + } + } + + let result = super::compare_directories(dir1.path(), dir2.path()); + + assert!(result.is_ok()); + } + #[test] fn compare_directories_different_file_names() { let dir1 = tempfile::tempdir().unwrap(); From 7ea7bd48861d91c5ec3d5776e379205225f29b83 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Mon, 2 Mar 2026 21:19:15 -0300 Subject: [PATCH 13/29] Add descriptive test case names --- crates/app/src/utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 2620e84b..0652ce64 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -140,9 +140,9 @@ mod tests { use std::{collections::HashMap, fs, io, path}; use test_case::test_case; - #[test_case(&hex::decode("433287d255abf237992d2279af5b1a1bb2c3d7124c97906edd848ebbb541a1c7").unwrap(), "433287d")] - #[test_case("aaa".as_bytes(), "616161")] - #[test_case("".as_bytes(), "")] + #[test_case(&hex::decode("433287d255abf237992d2279af5b1a1bb2c3d7124c97906edd848ebbb541a1c7").unwrap(), "433287d"; "full 32 bytes")] + #[test_case("aaa".as_bytes(), "616161"; "3 bytes")] + #[test_case("".as_bytes(), ""; "empty")] fn hex_7(bytes: &[u8], expected: &str) { let actual = super::hex_7(bytes); assert_eq!(actual, expected); From 3509b70397f9b69c27db71c0780253ebe01c04ba Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel <31224949+emlautarom1@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:47:45 -0300 Subject: [PATCH 14/29] Remove trailing comma Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- crates/app/src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 0652ce64..838b2b9c 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -451,7 +451,7 @@ mod tests { let result = super::compare_directories(dir1.path(), dir2.path()); - assert!(result.is_ok(),); + assert!(result.is_ok()); } /// Recursively copies all files and directories from `from` to `to`. From faff6dec9dd29b83900115e88f8b5023f456eed8 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 3 Mar 2026 08:21:34 -0300 Subject: [PATCH 15/29] Validate directory names --- crates/app/src/utils.rs | 72 +++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 838b2b9c..1100343c 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -16,16 +16,16 @@ pub enum UtilsError { DirectoryEntryCountMismatch(usize, usize), /// Unexpected file contents. - #[error("File content mismatch: {0} vs {1}")] - FileContentMismatch(path::PathBuf, path::PathBuf), + #[error("Content mismatch: {0} vs {1}")] + ContentMismatch(path::PathBuf, path::PathBuf), - /// Unexpected path. - #[error("File name mismatch: expected {0}, found {1}")] - FileNameMismatch(String, String), + /// Name mismatch. + #[error("Name mismatch: expected {0}, found {1}")] + NameMismatch(String, String), /// One entry is a file and the other is a directory for a given path. #[error("Type mismatch: {0} vs {1}")] - PathTypeMismatch(path::PathBuf, path::PathBuf), + TypeMismatch(path::PathBuf, path::PathBuf), } type Result = std::result::Result; @@ -110,25 +110,25 @@ pub fn compare_directories( let path1 = entry1.path(); let path2 = entry2.path(); + let name1 = entry1.file_name(); + let name2 = entry2.file_name(); + if name1 != name2 { + return Err(UtilsError::NameMismatch( + name1.display().to_string(), + name2.display().to_string(), + )); + } + if path1.is_dir() && path2.is_dir() { compare_directories(&path1, &path2)?; } else if path1.is_file() && path2.is_file() { - let name1 = entry1.file_name(); - let name2 = entry2.file_name(); - if name1 != name2 { - return Err(UtilsError::FileNameMismatch( - name1.display().to_string(), - name2.display().to_string(), - )); - } - let content1 = fs::read(&path1)?; let content2 = fs::read(&path2)?; if content1 != content2 { - return Err(UtilsError::FileContentMismatch(path1, path2)); + return Err(UtilsError::ContentMismatch(path1, path2)); } } else { - return Err(UtilsError::PathTypeMismatch(path1, path2)); + return Err(UtilsError::TypeMismatch(path1, path2)); } } @@ -290,7 +290,7 @@ mod tests { assert!(matches!( result, - Err(super::UtilsError::FileContentMismatch(_, _)) + Err(super::UtilsError::ContentMismatch(_, _)) )); } @@ -314,7 +314,7 @@ mod tests { assert!(matches!( result, - Err(super::UtilsError::FileContentMismatch(_, _)) + Err(super::UtilsError::ContentMismatch(_, _)) )); } @@ -354,10 +354,7 @@ mod tests { let result = super::compare_directories(dir1.path(), dir2.path()); - assert!(matches!( - result, - Err(super::UtilsError::PathTypeMismatch(_, _)) - )); + assert!(matches!(result, Err(super::UtilsError::TypeMismatch(_, _)))); } #[test] @@ -377,10 +374,7 @@ mod tests { let result = super::compare_directories(dir1.path(), dir2.path()); - assert!(matches!( - result, - Err(super::UtilsError::PathTypeMismatch(_, _)) - )); + assert!(matches!(result, Err(super::UtilsError::TypeMismatch(_, _)))); } #[test] @@ -438,10 +432,26 @@ mod tests { let result = super::compare_directories(dir1.path(), dir2.path()); - assert!(matches!( - result, - Err(super::UtilsError::FileNameMismatch(_, _)) - )); + assert!(matches!(result, Err(super::UtilsError::NameMismatch(_, _)))); + } + + #[test] + fn compare_directories_different_directory_names() { + let dir1 = tempfile::tempdir().unwrap(); + { + let some_dir_path = dir1.path().join("dir1"); + fs::create_dir_all(some_dir_path).unwrap(); + } + + let dir2 = tempfile::tempdir().unwrap(); + { + let some_dir_path = dir2.path().join("dir2"); + fs::create_dir_all(some_dir_path).unwrap(); + } + + let result = super::compare_directories(dir1.path(), dir2.path()); + + assert!(matches!(result, Err(super::UtilsError::NameMismatch(_, _)))); } #[test] From 807dee7bc90739ca5f047b267db8745006f88d72 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 3 Mar 2026 08:44:21 -0300 Subject: [PATCH 16/29] Ignore non dirs/files when extracting --- crates/app/src/utils.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 1100343c..b8b84782 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -74,15 +74,24 @@ pub fn extract_archive( let decompressor = flate2::read::GzDecoder::new(tar_gz); let mut archive = tar::Archive::new(decompressor); - // Extract each file, verifying that it does not exceed a reasonable size limit - // to prevent DoS attacks. - const MAX_FILE: u64 = 100 * 1024 * 1024; // 100MB limit per file for entry in archive.entries()? { let mut entry = entry?; - if entry.size() > MAX_FILE { - return Err(UtilsError::FileTooLarge(entry.path()?.to_path_buf())); + let entry_type = entry.header().entry_type(); + + if entry_type.is_dir() { + entry.unpack_in(&target_path)?; + } else if entry_type.is_file() { + // Check file size to prevent decompression bombs + const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024; // 100MB limit per file + if entry.size() > MAX_FILE_SIZE { + return Err(UtilsError::FileTooLarge(entry.path()?.to_path_buf())); + } + + entry.unpack_in(&target_path)?; + } else { + // Skip other types (symlinks, etc.) + continue; } - entry.unpack_in(&target_path)?; } Ok(()) From 0bc533bd268eb7ecca352ce644ccd72074d1ad49 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 4 Mar 2026 16:01:55 -0300 Subject: [PATCH 17/29] Use named fields for errors --- crates/app/src/utils.rs | 93 +++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index b8b84782..ff10d1d2 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -12,20 +12,40 @@ pub enum UtilsError { FileTooLarge(path::PathBuf), /// Directories have different number of entries. - #[error("Directory entry count mismatch: expected {0}, found {1}")] - DirectoryEntryCountMismatch(usize, usize), + #[error("Directory entry count mismatch: expected {expected}, found {found}")] + DirectoryEntryCountMismatch { + /// Expected number of entries. + expected: usize, + /// Actual number of entries + found: usize, + }, /// Unexpected file contents. - #[error("Content mismatch: {0} vs {1}")] - ContentMismatch(path::PathBuf, path::PathBuf), + #[error("Content mismatch: {expected} vs {found}")] + ContentMismatch { + /// Expected file path. + expected: path::PathBuf, + /// Actual file path. + found: path::PathBuf, + }, /// Name mismatch. - #[error("Name mismatch: expected {0}, found {1}")] - NameMismatch(String, String), + #[error("Name mismatch: expected {expected}, found {found}")] + NameMismatch { + /// Expected name. + expected: String, + /// Actual name. + found: String, + }, /// One entry is a file and the other is a directory for a given path. - #[error("Type mismatch: {0} vs {1}")] - TypeMismatch(path::PathBuf, path::PathBuf), + #[error("Type mismatch: {path1} vs {path2}")] + TypeMismatch { + /// First path. + path1: path::PathBuf, + /// Second path. + path2: path::PathBuf, + }, } type Result = std::result::Result; @@ -109,10 +129,10 @@ pub fn compare_directories( entries2.sort_by_key(|e| e.file_name()); if entries1.len() != entries2.len() { - return Err(UtilsError::DirectoryEntryCountMismatch( - entries1.len(), - entries2.len(), - )); + return Err(UtilsError::DirectoryEntryCountMismatch { + expected: entries1.len(), + found: entries2.len(), + }); } for (entry1, entry2) in entries1.iter().zip(entries2.iter()) { @@ -122,10 +142,10 @@ pub fn compare_directories( let name1 = entry1.file_name(); let name2 = entry2.file_name(); if name1 != name2 { - return Err(UtilsError::NameMismatch( - name1.display().to_string(), - name2.display().to_string(), - )); + return Err(UtilsError::NameMismatch { + expected: name1.display().to_string(), + found: name2.display().to_string(), + }); } if path1.is_dir() && path2.is_dir() { @@ -134,10 +154,13 @@ pub fn compare_directories( let content1 = fs::read(&path1)?; let content2 = fs::read(&path2)?; if content1 != content2 { - return Err(UtilsError::ContentMismatch(path1, path2)); + return Err(UtilsError::ContentMismatch { + expected: path1, + found: path2, + }); } } else { - return Err(UtilsError::TypeMismatch(path1, path2)); + return Err(UtilsError::TypeMismatch { path1, path2 }); } } @@ -275,7 +298,10 @@ mod tests { assert!(matches!( result, - Err(super::UtilsError::DirectoryEntryCountMismatch(1, 0)) + Err(super::UtilsError::DirectoryEntryCountMismatch { + expected: 1, + found: 0 + }) )); } @@ -299,7 +325,7 @@ mod tests { assert!(matches!( result, - Err(super::UtilsError::ContentMismatch(_, _)) + Err(super::UtilsError::ContentMismatch { .. }) )); } @@ -323,7 +349,7 @@ mod tests { assert!(matches!( result, - Err(super::UtilsError::ContentMismatch(_, _)) + Err(super::UtilsError::ContentMismatch { .. }) )); } @@ -342,7 +368,10 @@ mod tests { assert!(matches!( result, - Err(super::UtilsError::DirectoryEntryCountMismatch(_, _)) + Err(super::UtilsError::DirectoryEntryCountMismatch { + expected: 1, + found: 0 + }) )); } @@ -363,7 +392,10 @@ mod tests { let result = super::compare_directories(dir1.path(), dir2.path()); - assert!(matches!(result, Err(super::UtilsError::TypeMismatch(_, _)))); + assert!(matches!( + result, + Err(super::UtilsError::TypeMismatch { .. }) + )); } #[test] @@ -383,7 +415,10 @@ mod tests { let result = super::compare_directories(dir1.path(), dir2.path()); - assert!(matches!(result, Err(super::UtilsError::TypeMismatch(_, _)))); + assert!(matches!( + result, + Err(super::UtilsError::TypeMismatch { .. }) + )); } #[test] @@ -441,7 +476,10 @@ mod tests { let result = super::compare_directories(dir1.path(), dir2.path()); - assert!(matches!(result, Err(super::UtilsError::NameMismatch(_, _)))); + assert!(matches!( + result, + Err(super::UtilsError::NameMismatch { .. }) + )); } #[test] @@ -460,7 +498,10 @@ mod tests { let result = super::compare_directories(dir1.path(), dir2.path()); - assert!(matches!(result, Err(super::UtilsError::NameMismatch(_, _)))); + assert!(matches!( + result, + Err(super::UtilsError::NameMismatch { .. }) + )); } #[test] From 893c2445cd2c7ff6e3fe102ba7491e42fab6a437 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Thu, 5 Mar 2026 12:30:06 -0300 Subject: [PATCH 18/29] Use streaming for file comparison --- crates/app/src/utils.rs | 59 +++++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index ff10d1d2..d5aeb33a 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -151,14 +151,7 @@ pub fn compare_directories( if path1.is_dir() && path2.is_dir() { compare_directories(&path1, &path2)?; } else if path1.is_file() && path2.is_file() { - let content1 = fs::read(&path1)?; - let content2 = fs::read(&path2)?; - if content1 != content2 { - return Err(UtilsError::ContentMismatch { - expected: path1, - found: path2, - }); - } + compare_file_contents(&path1, &path2)?; } else { return Err(UtilsError::TypeMismatch { path1, path2 }); } @@ -167,6 +160,56 @@ pub fn compare_directories( Ok(()) } +/// Compare two files for equality. +fn compare_file_contents(path1: &path::PathBuf, path2: &path::PathBuf) -> Result<()> { + let error = Err(UtilsError::ContentMismatch { + expected: path1.clone(), + found: path2.clone(), + }); + + // Fast path: compare metadata first + let metadata1 = fs::metadata(&path1)?; + let metadata2 = fs::metadata(&path2)?; + + if metadata1.len() != metadata2.len() { + return error; + } + + // For small files, read into memory + const SMALL_FILE_THRESHOLD: u64 = 5 * 1024 * 1024; // 5MB + if metadata1.len() < SMALL_FILE_THRESHOLD { + let content1 = fs::read(&path1)?; + let content2 = fs::read(&path2)?; + if content1 != content2 { + return error; + } + } else { + // Stream comparison for large files + use std::io::Read; + let mut file1 = fs::File::open(&path1)?; + let mut file2 = fs::File::open(&path2)?; + + const BUFFER_SIZE: usize = 8192; + let mut buf1 = [0u8; BUFFER_SIZE]; + let mut buf2 = [0u8; BUFFER_SIZE]; + + loop { + let n1 = file1.read(&mut buf1)?; + let n2 = file2.read(&mut buf2)?; + + if n1 != n2 || buf1[..n1] != buf2[..n2] { + return error; + } + + if n1 == 0 { + break; // EOF + } + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use std::{collections::HashMap, fs, io, path}; From 9cb07aace67db463ee3da9c56e6ed901f2647dd6 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Thu, 5 Mar 2026 12:54:15 -0300 Subject: [PATCH 19/29] Fix clippy suggestions --- crates/app/src/utils.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index d5aeb33a..09fb3d7f 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -168,8 +168,8 @@ fn compare_file_contents(path1: &path::PathBuf, path2: &path::PathBuf) -> Result }); // Fast path: compare metadata first - let metadata1 = fs::metadata(&path1)?; - let metadata2 = fs::metadata(&path2)?; + let metadata1 = fs::metadata(path1)?; + let metadata2 = fs::metadata(path2)?; if metadata1.len() != metadata2.len() { return error; @@ -178,16 +178,16 @@ fn compare_file_contents(path1: &path::PathBuf, path2: &path::PathBuf) -> Result // For small files, read into memory const SMALL_FILE_THRESHOLD: u64 = 5 * 1024 * 1024; // 5MB if metadata1.len() < SMALL_FILE_THRESHOLD { - let content1 = fs::read(&path1)?; - let content2 = fs::read(&path2)?; + let content1 = fs::read(path1)?; + let content2 = fs::read(path2)?; if content1 != content2 { return error; } } else { // Stream comparison for large files use std::io::Read; - let mut file1 = fs::File::open(&path1)?; - let mut file2 = fs::File::open(&path2)?; + let mut file1 = fs::File::open(path1)?; + let mut file2 = fs::File::open(path2)?; const BUFFER_SIZE: usize = 8192; let mut buf1 = [0u8; BUFFER_SIZE]; From 4dc4de490ee4866f7481b2f04ee513ff56a97dcb Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Thu, 5 Mar 2026 13:47:45 -0300 Subject: [PATCH 20/29] Naming convention --- crates/app/src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 09fb3d7f..1cb86e90 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -5,7 +5,7 @@ use std::{fs, io, path}; pub enum UtilsError { /// Underlying IO error occurred. #[error("IO error: {0}")] - IOError(#[from] io::Error), + IoError(#[from] io::Error), /// File exceeds the maximum allowed size during extraction. #[error("File too large: {0}")] From c33a7ec188b38903ec58ce6703a8a7b8f6711cfa Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Thu, 5 Mar 2026 13:50:42 -0300 Subject: [PATCH 21/29] Prevent path traversal due to filename --- crates/app/src/utils.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 1cb86e90..783baa11 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -11,6 +11,10 @@ pub enum UtilsError { #[error("File too large: {0}")] FileTooLarge(path::PathBuf), + /// Illegal filename (attempts directory traversal) + #[error("Output file must be directly within the target directory")] + IllegalFilename, + /// Directories have different number of entries. #[error("Directory entry count mismatch: expected {expected}, found {found}")] DirectoryEntryCountMismatch { @@ -63,6 +67,12 @@ pub fn bundle_output( target_path: impl AsRef, filename: impl AsRef, ) -> Result<()> { + // Compute and validate the output path + let output_path = path::Path::new(target_path.as_ref()).join(filename.as_ref()); + if output_path.parent() != Some(target_path.as_ref()) { + return Err(UtilsError::IllegalFilename); + } + // Create output file let tar_file = tempfile::NamedTempFile::new()?; let tar_file_path = tar_file.path().to_owned(); @@ -78,7 +88,6 @@ pub fn bundle_output( fs::create_dir_all(&target_path)?; // Move the created tarball to the target location - let output_path = path::Path::new(target_path.as_ref()).join(filename.as_ref()); fs::rename(tar_file_path, output_path)?; Ok(()) From d9b564e1c8e44158703bfc81dbca3763c2e3cbab Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:15:40 +0100 Subject: [PATCH 22/29] feat(p2p): add upgrade to quic (#259) * feat: add quic upgrade * feat: add comment about peer addresses * fix: linter * fix: clean up tests * fix: linter * fix: review comments * fix: review comments --- crates/p2p/examples/quic_upgrade.rs | 248 ++++++++++++++++ crates/p2p/src/behaviours/pluto.rs | 29 +- crates/p2p/src/conn_logger.rs | 22 +- crates/p2p/src/lib.rs | 3 + crates/p2p/src/p2p.rs | 14 +- crates/p2p/src/p2p_context.rs | 30 +- crates/p2p/src/quic_upgrade.rs | 431 ++++++++++++++++++++++++++++ crates/p2p/src/utils.rs | 28 ++ 8 files changed, 794 insertions(+), 11 deletions(-) create mode 100644 crates/p2p/examples/quic_upgrade.rs create mode 100644 crates/p2p/src/quic_upgrade.rs diff --git a/crates/p2p/examples/quic_upgrade.rs b/crates/p2p/examples/quic_upgrade.rs new file mode 100644 index 00000000..09e91ca0 --- /dev/null +++ b/crates/p2p/examples/quic_upgrade.rs @@ -0,0 +1,248 @@ +//! QUIC Upgrade Example +//! +//! This example demonstrates the QUIC upgrade behaviour that automatically +//! upgrades TCP connections to QUIC when both peers support it. +//! +//! # Running the Example +//! +//! Run two instances in separate terminals: +//! +//! ```bash +//! # Terminal 1: Start the first node (note the peer ID printed) +//! cargo run -p pluto-p2p --example quic_upgrade -- --port 9000 +//! # Output: Started node with peer ID: 16Uiu2HAmXXX... +//! +//! # Terminal 2: Start the second node with the first node's peer ID as known peer +//! cargo run -p pluto-p2p --example quic_upgrade -- --port 9001 \ +//! --dial /ip4/127.0.0.1/tcp/9000 \ +//! --peer 16Uiu2HAmXXX... +//! ``` +//! +//! # What Happens +//! +//! 1. Node 2 connects to Node 1 via TCP +//! 2. Both nodes exchange identify information (including their QUIC addresses) +//! 3. The peer addresses are stored in the peer store +//! 4. The QUIC upgrade behaviour detects the TCP connection to a known peer +//! 5. After ~1 minute, it attempts to dial the peer's QUIC address +//! 6. On success: the redundant TCP connection is closed +//! 7. On failure: exponential backoff is applied before retrying +//! +//! # Note +//! +//! The QUIC upgrade behaviour only upgrades connections to "known peers" +//! (cluster members). In production, these are configured at startup. +//! For this example, use `--peer` to specify the peer ID to upgrade. +//! +//! # Expected Output +//! +//! ```text +//! [CONNECTED] 16Uiu2HAm... via TCP at /ip4/127.0.0.1/tcp/9000 +//! [IDENTIFY] Received from 16Uiu2HAm... +//! Agent: quic-upgrade-example/1.0.0 +//! Addresses: +//! - [TCP] /ip4/127.0.0.1/tcp/9000 +//! - [QUIC] /ip4/127.0.0.1/udp/9000/quic-v1 +//! ... (after ~1 minute) ... +//! [CONNECTED] 16Uiu2HAm... via QUIC at /ip4/127.0.0.1/udp/9000/quic-v1 +//! [QUIC UPGRADE] Successfully upgraded connection to 16Uiu2HAm...! +//! [DISCONNECTED] 16Uiu2HAm... TCP connection closed (remaining: 1) +//! ``` + +use std::str::FromStr; + +use anyhow::Result; +use clap::Parser; +use k256::elliptic_curve::rand_core::OsRng; +use libp2p::{Multiaddr, PeerId, futures::StreamExt, relay, swarm::SwarmEvent}; +use pluto_p2p::{ + behaviours::pluto::PlutoBehaviourEvent, + config::P2PConfig, + p2p::{Node, NodeType}, + quic_upgrade::QuicUpgradeEvent, +}; +use tokio::signal; + +/// Command line arguments. +#[derive(Debug, Parser)] +#[command(name = "quic_upgrade")] +#[command(about = "Demonstrates QUIC upgrade behaviour")] +pub struct Args { + /// The port to listen on (both TCP and UDP/QUIC). + #[arg(short, long, default_value = "9000")] + pub port: u16, + + /// Address to dial (e.g., /ip4/127.0.0.1/tcp/9000). + #[arg(short, long)] + pub dial: Option, + + /// Known peer ID(s) to attempt QUIC upgrade for. + /// The upgrade behaviour only upgrades connections to known peers. + #[arg(long)] + pub peer: Vec, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + let key = k256::SecretKey::random(&mut OsRng); + + // Create a config with the specified port + // Note: P2PConfig requires a specific IP (not 0.0.0.0), so we use 127.0.0.1 for + // local testing + let config = P2PConfig { + tcp_addrs: vec![format!("127.0.0.1:{}", args.port)], + udp_addrs: vec![format!("127.0.0.1:{}", args.port)], + ..Default::default() + }; + + // Parse known peer IDs from command line + let known_peers: Vec = args + .peer + .iter() + .filter_map(|s| PeerId::from_str(s).ok()) + .collect(); + + if !known_peers.is_empty() { + println!("Known peers for QUIC upgrade: {:?}", known_peers); + } + + let mut node = Node::new( + config, + key, + NodeType::QUIC, // Enable QUIC transport + false, // Don't filter private addresses (for local testing) + known_peers, + |builder, _keypair, relay_client| { + builder + .with_user_agent("quic-upgrade-example/1.0.0") + .with_quic_enabled(true) // Enable QUIC upgrade behaviour + .with_inner(relay_client) + }, + )?; + + println!("Started node with peer ID: {}", node.local_peer_id()); + println!("Listening on TCP and QUIC port: {}", args.port); + + // Dial the remote peer if specified + if let Some(dial_addr) = &args.dial { + println!("Dialing remote peer via TCP: {dial_addr}"); + node.dial(dial_addr.clone())?; + } + + println!("\nWaiting for events... (Ctrl+C to quit)"); + if args.peer.is_empty() { + println!("Note: No --peer specified. QUIC upgrade only works for known peers."); + println!(" Copy this node's peer ID and pass it to the other node with --peer\n"); + } else { + println!("QUIC upgrade will be attempted ~1 minute after TCP connection is established.\n"); + } + + // Event loop + loop { + tokio::select! { + event = node.select_next_some() => { + handle_event(event); + } + _ = signal::ctrl_c() => { + println!("\nReceived Ctrl+C, shutting down..."); + break; + } + } + } + + Ok(()) +} + +fn handle_event(event: SwarmEvent>) { + match event { + // New listen address + SwarmEvent::NewListenAddr { address, .. } => { + println!("[LISTEN] {address}"); + } + + // Connection established + SwarmEvent::ConnectionEstablished { + peer_id, endpoint, .. + } => { + let addr = match &endpoint { + libp2p::core::ConnectedPoint::Dialer { address, .. } => address, + libp2p::core::ConnectedPoint::Listener { send_back_addr, .. } => send_back_addr, + }; + let transport = if addr.to_string().contains("quic") { + "QUIC" + } else { + "TCP" + }; + println!("[CONNECTED] {peer_id} via {transport} at {addr}"); + } + + // Connection closed + SwarmEvent::ConnectionClosed { + peer_id, + endpoint, + num_established, + .. + } => { + let addr = match &endpoint { + libp2p::core::ConnectedPoint::Dialer { address, .. } => address, + libp2p::core::ConnectedPoint::Listener { send_back_addr, .. } => send_back_addr, + }; + let transport = if addr.to_string().contains("quic") { + "QUIC" + } else { + "TCP" + }; + println!( + "[DISCONNECTED] {peer_id} {transport} connection closed (remaining: {num_established})" + ); + } + + // QUIC upgrade events + SwarmEvent::Behaviour(PlutoBehaviourEvent::QuicUpgrade(event)) => match event { + QuicUpgradeEvent::Upgraded { peer } => { + println!("[QUIC UPGRADE] Successfully upgraded connection to {peer}!"); + } + QuicUpgradeEvent::UpgradeFailed { peer, reason } => { + println!("[QUIC UPGRADE FAILED] {peer}: {reason}"); + } + }, + + // Identify received - shows peer's addresses including QUIC + SwarmEvent::Behaviour(PlutoBehaviourEvent::Identify( + libp2p::identify::Event::Received { peer_id, info, .. }, + )) => { + println!("[IDENTIFY] Received from {peer_id}"); + println!(" Agent: {}", info.agent_version); + println!(" Addresses:"); + for addr in &info.listen_addrs { + let transport = if addr.to_string().contains("quic") { + "QUIC" + } else if addr.to_string().contains("tcp") { + "TCP" + } else { + "other" + }; + println!(" - [{transport}] {addr}"); + } + } + + // Ping events + SwarmEvent::Behaviour(PlutoBehaviourEvent::Ping(event)) => { + if let Ok(rtt) = event.result { + println!("[PING] {} RTT: {:?}", event.peer, rtt); + } + } + + // Connection errors + SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { + println!("[ERROR] Outgoing connection to {peer_id:?}: {error}"); + } + SwarmEvent::IncomingConnectionError { error, .. } => { + println!("[ERROR] Incoming connection: {error}"); + } + + // Ignore other events + _ => {} + } +} diff --git a/crates/p2p/src/behaviours/pluto.rs b/crates/p2p/src/behaviours/pluto.rs index 6da41dc1..710bd511 100644 --- a/crates/p2p/src/behaviours/pluto.rs +++ b/crates/p2p/src/behaviours/pluto.rs @@ -12,6 +12,7 @@ use crate::{ conn_logger::{ConnectionLoggerBehaviour, DefaultConnectionLoggerMetrics}, gater::ConnGater, p2p_context::P2PContext, + quic_upgrade::QuicUpgradeBehaviour, }; pub use super::optional::OptionalBehaviour; @@ -39,6 +40,7 @@ pub const DEFAULT_IDENTIFY_CACHE_SIZE: usize = 100; /// - **Identify**: Exchanges peer information and supported protocols /// - **Ping**: Measures latency and keeps connections alive /// - **AutoNAT**: Detects NAT status and public reachability +/// - **QUIC upgrade**: Periodically upgrades TCP connections to QUIC #[derive(NetworkBehaviour)] pub struct PlutoBehaviour { /// Connection logger behaviour - MUST be first so peer store is updated @@ -52,6 +54,8 @@ pub struct PlutoBehaviour { pub ping: ping::Behaviour, /// AutoNAT behaviour for NAT detection. pub autonat: autonat::Behaviour, + /// QUIC upgrade behaviour for upgrading TCP to QUIC connections. + pub quic_upgrade: QuicUpgradeBehaviour, /// Inner behaviour. pub inner: OptionalBehaviour, } @@ -70,6 +74,7 @@ impl PlutoBehaviour { /// - **Identify**: Protocol and agent identification /// - **Ping**: Latency measurement and keepalive /// - **AutoNAT**: NAT traversal detection +/// - **QUIC upgrade**: Periodic TCP to QUIC connection upgrades #[derive(Debug, Clone)] pub struct PlutoBehaviourBuilder { // Gater config @@ -84,6 +89,9 @@ pub struct PlutoBehaviourBuilder { p2p_context: P2PContext, + // QUIC upgrade config + quic_enabled: bool, + // Inner behaviour inner: Option, } @@ -96,6 +104,7 @@ impl Default for PlutoBehaviourBuilder { user_agent: DEFAULT_USER_AGENT.clone(), autonat_config: autonat::Config::default(), p2p_context: P2PContext::default(), + quic_enabled: false, inner: None, } } @@ -166,19 +175,30 @@ impl PlutoBehaviourBuilder { self } + /// Sets whether QUIC is enabled. + /// + /// When enabled, the behaviour will periodically attempt to upgrade + /// TCP connections to QUIC connections. + pub fn with_quic_enabled(mut self, enabled: bool) -> Self { + self.quic_enabled = enabled; + self + } + /// Builds the [`PlutoBehaviour`] with the provided keypair. /// /// # Arguments /// /// * `key` - The keypair for this node, used for identify and autonat pub fn build(self, key: &Keypair) -> PlutoBehaviour { + let local_peer_id = key.public().to_peer_id(); + let identify_config = identify::Config::new(self.identify_protocol, key.public()) .with_agent_version(self.user_agent) .with_interval(DEFAULT_IDENTIFY_INTERVAL) .with_cache_size(DEFAULT_IDENTIFY_CACHE_SIZE); PlutoBehaviour { - conn_logger: ConnectionLoggerBehaviour::new(self.p2p_context), + conn_logger: ConnectionLoggerBehaviour::new(self.p2p_context.clone()), gater: self.gater.unwrap_or_else(ConnGater::new_open_gater), identify: identify::Behaviour::new(identify_config), ping: ping::Behaviour::new( @@ -186,7 +206,12 @@ impl PlutoBehaviourBuilder { .with_interval(DEFAULT_PING_INTERVAL) .with_timeout(DEFAULT_PING_TIMEOUT), ), - autonat: autonat::Behaviour::new(key.public().to_peer_id(), self.autonat_config), + autonat: autonat::Behaviour::new(local_peer_id, self.autonat_config), + quic_upgrade: QuicUpgradeBehaviour::new( + self.p2p_context, + local_peer_id, + self.quic_enabled, + ), inner: self.inner.into(), } } diff --git a/crates/p2p/src/conn_logger.rs b/crates/p2p/src/conn_logger.rs index dfd5da86..7375f76c 100644 --- a/crates/p2p/src/conn_logger.rs +++ b/crates/p2p/src/conn_logger.rs @@ -219,11 +219,19 @@ impl NetworkBehaviour for ConnectionLogger other_established = event.other_established, "connection established" ); + // Extract remote address from the endpoint + let remote_addr = match &event.endpoint { + libp2p::core::ConnectedPoint::Dialer { address, .. } => address.clone(), + libp2p::core::ConnectedPoint::Listener { send_back_addr, .. } => { + send_back_addr.clone() + } + }; // Update peer store - this is done here so all other behaviours // see the updated peer store immediately self.p2p_context.peer_store_write_lock().add_peer(Peer { id: event.peer_id, connection_id: event.connection_id, + remote_addr, }); } libp2p::swarm::FromSwarm::ConnectionClosed(event) => { @@ -233,17 +241,21 @@ impl NetworkBehaviour for ConnectionLogger num_established = event.remaining_established, "connection closed" ); + // Extract remote address from the endpoint + let addr = match &event.endpoint { + libp2p::core::ConnectedPoint::Dialer { address, .. } => address.clone(), + libp2p::core::ConnectedPoint::Listener { send_back_addr, .. } => { + send_back_addr.clone() + } + }; // Update peer store self.p2p_context.peer_store_write_lock().remove_peer(Peer { id: event.peer_id, connection_id: event.connection_id, + remote_addr: addr.clone(), }); // Decrement the connection count based on the endpoint address - let addr = match &event.endpoint { - libp2p::core::ConnectedPoint::Dialer { address, .. } => address, - libp2p::core::ConnectedPoint::Listener { send_back_addr, .. } => send_back_addr, - }; - self.decrement_connection(event.peer_id, addr); + self.decrement_connection(event.peer_id, &addr); } _ => {} } diff --git a/crates/p2p/src/lib.rs b/crates/p2p/src/lib.rs index 7d8c8128..3be87dbc 100644 --- a/crates/p2p/src/lib.rs +++ b/crates/p2p/src/lib.rs @@ -40,3 +40,6 @@ pub mod conn_logger; /// Global context. pub mod p2p_context; + +/// QUIC connection upgrade behaviour. +pub mod quic_upgrade; diff --git a/crates/p2p/src/p2p.rs b/crates/p2p/src/p2p.rs index ece514f8..a35a2775 100644 --- a/crates/p2p/src/p2p.rs +++ b/crates/p2p/src/p2p.rs @@ -93,7 +93,7 @@ use std::{ use futures::{Stream, StreamExt, stream::FusedStream}; use libp2p::{ - Multiaddr, PeerId, Swarm, SwarmBuilder, autonat, + Multiaddr, PeerId, Swarm, SwarmBuilder, autonat, identify, identity::Keypair, noise, ping, relay, swarm::{NetworkBehaviour, SwarmEvent}, @@ -495,6 +495,18 @@ impl Node { /// Handles a swarm event to update metrics and logging. fn handle_event(&mut self, event: &SwarmEvent>) { match event { + // Identify - update peer addresses in the peer store + SwarmEvent::Behaviour(PlutoBehaviourEvent::Identify(identify::Event::Received { + peer_id, + info, + .. + })) => { + // The peer addresses will be available in the next poll of the node. + self.p2p_context + .peer_store_write_lock() + .set_peer_addresses(*peer_id, info.listen_addrs.clone()); + } + // Ping metrics SwarmEvent::Behaviour(PlutoBehaviourEvent::Ping(ping::Event { peer, result, .. diff --git a/crates/p2p/src/p2p_context.rs b/crates/p2p/src/p2p_context.rs index 4dfcdb4c..098276a4 100644 --- a/crates/p2p/src/p2p_context.rs +++ b/crates/p2p/src/p2p_context.rs @@ -1,9 +1,9 @@ use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, }; -use libp2p::{PeerId, swarm::ConnectionId}; +use libp2p::{Multiaddr, PeerId, swarm::ConnectionId}; /// Global context shared across P2P components. /// @@ -50,7 +50,7 @@ impl P2PContext { } } -/// Peer. +/// Peer connection information. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Peer { /// Peer ID. @@ -58,6 +58,9 @@ pub struct Peer { /// Connection ID. pub connection_id: ConnectionId, + + /// Remote address of the connection. + pub remote_addr: Multiaddr, } /// Peer store. @@ -68,6 +71,9 @@ pub struct PeerStore { /// Inactive peers. inactive_peers: HashSet, + + /// Known addresses for each peer (populated from identify protocol). + peer_addresses: HashMap>, } impl PeerStore { @@ -111,4 +117,22 @@ impl PeerStore { pub fn inactive_count(&self) -> usize { self.inactive_peers.len() } + + /// Returns all active connections to a specific peer. + pub fn connections_to_peer(&self, peer_id: &PeerId) -> Vec<&Peer> { + self.active_peers + .iter() + .filter(|p| &p.id == peer_id) + .collect() + } + + /// Sets the known addresses for a peer (from identify protocol). + pub fn set_peer_addresses(&mut self, peer_id: PeerId, addrs: Vec) { + self.peer_addresses.insert(peer_id, addrs); + } + + /// Returns the known addresses for a peer. + pub fn peer_addresses(&self, peer_id: &PeerId) -> Option<&Vec> { + self.peer_addresses.get(peer_id) + } } diff --git a/crates/p2p/src/quic_upgrade.rs b/crates/p2p/src/quic_upgrade.rs new file mode 100644 index 00000000..0bcae00a --- /dev/null +++ b/crates/p2p/src/quic_upgrade.rs @@ -0,0 +1,431 @@ +//! QUIC connection upgrade behaviour. + +use std::{ + collections::{HashMap, VecDeque}, + convert::Infallible, + task::{Context, Poll}, + time::Duration, +}; + +use libp2p::{ + Multiaddr, PeerId, + swarm::{ + ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, THandlerInEvent, + THandlerOutEvent, ToSwarm, + dial_opts::{DialOpts, PeerCondition}, + dummy, + }, +}; +use tokio::time::Interval; +use tracing::{debug, info}; + +use crate::{ + name::peer_name, + p2p_context::P2PContext, + utils::{ + filter_direct_quic_addrs, has_direct_quic_conn, has_direct_tcp_conn, is_quic_addr, + is_relay_addr, is_tcp_addr, + }, +}; + +/// Interval between QUIC upgrade attempts (1 minute). +const UPGRADE_INTERVAL: Duration = Duration::from_secs(60); + +/// Backoff state for a peer's QUIC upgrade attempts. +#[derive(Debug, Clone)] +struct QuicUpgradeBackoff { + /// Number of tickers (minutes) remaining before next upgrade attempt. + tickers_remaining: u32, + /// Current backoff duration in minutes/tickers. + backoff_duration: u32, +} + +impl QuicUpgradeBackoff { + /// Initial backoff duration (1 minute, becomes 2 minutes after first + /// failure). + const INITIAL: u32 = 1; + /// Maximum backoff duration (512 minutes / ~8 hours). + const MAX: u32 = 512; + + /// Creates a new backoff state with initial values. + fn new() -> Self { + Self { + tickers_remaining: Self::INITIAL, + backoff_duration: Self::INITIAL, + } + } + + /// Records a failure, doubling the backoff duration up to the maximum. + fn record_failure(&mut self) { + self.backoff_duration = self.backoff_duration.saturating_mul(2).min(Self::MAX); + self.tickers_remaining = self.backoff_duration; + } +} + +/// Events emitted by the QUIC upgrade behaviour. +#[derive(Debug, Clone)] +pub enum QuicUpgradeEvent { + /// Successfully upgraded to QUIC. + Upgraded { + /// The peer that was upgraded. + peer: PeerId, + }, + /// Upgrade failed. + UpgradeFailed { + /// The peer that failed to upgrade. + peer: PeerId, + /// The reason for the failure. + reason: String, + }, +} + +/// State of an in-progress upgrade attempt. +#[derive(Debug)] +enum UpgradeState { + /// Waiting for QUIC connection to be established. + DialingQuic { + /// TCP connection IDs to close after QUIC is established. + tcp_conn_ids: Vec, + }, +} + +/// QUIC connection upgrade behaviour. +/// +/// Periodically (every 1 minute) attempts to upgrade direct TCP connections +/// to QUIC connections with exponential backoff on failures. +/// +/// The behaviour assumes that the peer store is updated correctly and will work +/// only with the given set of known peers. +/// +/// # Upgrade Logic +/// +/// For each known peer: +/// 1. Skip if already has direct QUIC connection (close redundant TCP) +/// 2. Skip if no direct TCP connection +/// 3. Skip if no known QUIC addresses +/// 4. Attempt to dial QUIC addresses +/// 5. On success, close redundant TCP connections +/// 6. On failure, apply exponential backoff +pub struct QuicUpgradeBehaviour { + /// P2P context for accessing peer store and known peers. + p2p_context: P2PContext, + /// Local peer ID (to skip self). + local_peer_id: PeerId, + /// Backoff state per peer. + backoffs: HashMap, + /// Pending events to emit. + pending_events: VecDeque>, + /// In-progress upgrade attempts. + pending_upgrades: HashMap, + /// Interval timer for running upgrade logic periodically. + ticker: Interval, + /// Whether QUIC is enabled on this node. + quic_enabled: bool, +} + +impl std::fmt::Debug for QuicUpgradeBehaviour { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("QuicUpgradeBehaviour") + .field("local_peer_id", &self.local_peer_id) + .field("backoffs", &self.backoffs) + .field("pending_events", &self.pending_events.len()) + .field("pending_upgrades", &self.pending_upgrades.len()) + .field("ticker", &"") + .field("quic_enabled", &self.quic_enabled) + .finish() + } +} + +impl QuicUpgradeBehaviour { + /// Creates a new QUIC upgrade behaviour. + /// + /// # Arguments + /// + /// * `p2p_context` - Shared P2P context for accessing peer store + /// * `local_peer_id` - Local peer ID to skip self + /// * `quic_enabled` - Whether QUIC is enabled on this node + pub fn new(p2p_context: P2PContext, local_peer_id: PeerId, quic_enabled: bool) -> Self { + let mut ticker = tokio::time::interval(UPGRADE_INTERVAL); + ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + Self { + p2p_context, + local_peer_id, + backoffs: HashMap::new(), + pending_events: VecDeque::new(), + pending_upgrades: HashMap::new(), + ticker, + quic_enabled, + } + } + + /// Checks if a peer should skip upgrade due to active backoff. + fn should_skip(&mut self, peer: &PeerId) -> bool { + if let Some(backoff) = self.backoffs.get_mut(peer) + && backoff.tickers_remaining > 0 + { + backoff.tickers_remaining = backoff.tickers_remaining.saturating_sub(1); + debug!( + peer = %peer_name(peer), + remaining = backoff.tickers_remaining, + backoff_duration_minutes = backoff.backoff_duration, + "skipping QUIC upgrade due to backoff" + ); + return true; + } + false + } + + /// Records a failure for a peer, applying exponential backoff. + fn record_failure(&mut self, peer: PeerId, reason: &str) { + self.backoffs + .entry(peer) + .or_insert_with(QuicUpgradeBackoff::new) + .record_failure(); + + self.pending_events + .push_back(ToSwarm::GenerateEvent(QuicUpgradeEvent::UpgradeFailed { + peer, + reason: reason.to_string(), + })); + } + + /// Clears backoff state for a peer after successful upgrade. + fn clear_backoff(&mut self, peer: &PeerId) { + self.backoffs.remove(peer); + } + + /// Runs the upgrade logic for all known peers. + fn run_upgrade_logic(&mut self) { + if !self.quic_enabled { + debug!("node doesn't have feature QUIC enabled"); + return; + } + + let peer_ids: Vec = self.p2p_context.known_peers().iter().copied().collect(); + + for peer_id in peer_ids { + if peer_id == self.local_peer_id + || self.should_skip(&peer_id) + || self.pending_upgrades.contains_key(&peer_id) + { + continue; + } + + let conns: Vec<_> = self + .p2p_context + .peer_store_lock() + .connections_to_peer(&peer_id) + .into_iter() + .cloned() + .collect(); + + if conns.is_empty() { + debug!( + peer = %peer_name(&peer_id), + "no connection to peer" + ); + continue; + } + + let conn_refs: Vec<_> = conns.iter().collect(); + + if has_direct_quic_conn(&conn_refs) { + debug!( + peer = %peer_name(&peer_id), + "already has direct QUIC connection to peer" + ); + + let tcp_conn_ids: Vec<_> = conns + .iter() + .filter(|c| is_tcp_addr(&c.remote_addr) && !is_relay_addr(&c.remote_addr)) + .map(|c| c.connection_id) + .collect(); + + debug!( + peer = %peer_name(&peer_id), + "closing {} redundant TCP connections after QUIC upgrade", + tcp_conn_ids.len() + ); + self.close_tcp_connections(peer_id, tcp_conn_ids); + + continue; + } + + if !has_direct_tcp_conn(&conn_refs) { + debug!( + peer = %peer_name(&peer_id), + "no direct connection via TCP to peer" + ); + continue; + } + + let quic_addrs = self + .p2p_context + .peer_store_lock() + .peer_addresses(&peer_id) + .map(|addrs| filter_direct_quic_addrs(addrs.iter().cloned())) + .unwrap_or_default(); + + if quic_addrs.is_empty() { + debug!( + peer = %peer_name(&peer_id), + "no known QUIC addresses to peer" + ); + continue; + } + + info!( + peer = %peer_name(&peer_id), + quic_addrs = ?quic_addrs, + "trying to upgrade to QUIC connection with peer" + ); + + let tcp_conn_ids: Vec<_> = conns + .iter() + .filter(|c| is_tcp_addr(&c.remote_addr) && !is_relay_addr(&c.remote_addr)) + .map(|c| c.connection_id) + .collect(); + + self.pending_upgrades + .insert(peer_id, UpgradeState::DialingQuic { tcp_conn_ids }); + + self.pending_events.push_back(ToSwarm::Dial { + opts: DialOpts::peer_id(peer_id) + .addresses(quic_addrs) + .condition(PeerCondition::Always) + .build(), + }); + } + } + + fn close_tcp_connections(&mut self, peer_id: PeerId, conn_ids: Vec) { + for conn_id in conn_ids { + self.pending_events.push_back(ToSwarm::CloseConnection { + peer_id, + connection: libp2p::swarm::CloseConnection::One(conn_id), + }); + } + } + + /// Handles a successful connection establishment. + fn handle_connection_established(&mut self, peer_id: PeerId, addr: &Multiaddr) { + if let Some(UpgradeState::DialingQuic { tcp_conn_ids }) = + self.pending_upgrades.remove(&peer_id) + { + if is_quic_addr(addr) && !is_relay_addr(addr) { + info!( + peer = %peer_name(&peer_id), + addr = %addr, + "upgraded connection to QUIC" + ); + + debug!( + peer = %peer_name(&peer_id), + "closing {} redundant TCP connections after QUIC upgrade", + tcp_conn_ids.len() + ); + self.close_tcp_connections(peer_id, tcp_conn_ids); + + self.clear_backoff(&peer_id); + self.pending_events + .push_back(ToSwarm::GenerateEvent(QuicUpgradeEvent::Upgraded { + peer: peer_id, + })); + } else { + debug!( + peer = %peer_name(&peer_id), + addr = %addr, + "connected via non-direct address instead of direct QUIC" + ); + self.record_failure( + peer_id, + "connected via non-direct address instead of direct QUIC", + ); + } + } + } + + /// Handles a dial failure. + fn handle_dial_failure(&mut self, peer_id: Option) { + if let Some(peer_id) = peer_id + && self.pending_upgrades.remove(&peer_id).is_some() + { + info!( + peer = %peer_name(&peer_id), + "failed to connect to peer during QUIC upgrade" + ); + self.record_failure(peer_id, "dial failed"); + } + } +} + +impl NetworkBehaviour for QuicUpgradeBehaviour { + type ConnectionHandler = dummy::ConnectionHandler; + type ToSwarm = QuicUpgradeEvent; + + fn handle_established_inbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _local_addr: &Multiaddr, + _remote_addr: &Multiaddr, + ) -> Result, ConnectionDenied> { + Ok(dummy::ConnectionHandler) + } + + fn handle_established_outbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _addr: &Multiaddr, + _role_override: libp2p::core::Endpoint, + _port_use: libp2p::core::transport::PortUse, + ) -> Result, ConnectionDenied> { + Ok(dummy::ConnectionHandler) + } + + fn on_swarm_event(&mut self, event: FromSwarm) { + match event { + FromSwarm::ConnectionEstablished(event) => { + let addr = match &event.endpoint { + libp2p::core::ConnectedPoint::Dialer { address, .. } => address, + libp2p::core::ConnectedPoint::Listener { send_back_addr, .. } => send_back_addr, + }; + self.handle_connection_established(event.peer_id, addr); + } + FromSwarm::DialFailure(event) => { + self.handle_dial_failure(event.peer_id); + } + _ => {} + } + } + + fn on_connection_handler_event( + &mut self, + _peer_id: PeerId, + _connection_id: ConnectionId, + _event: THandlerOutEvent, + ) { + // Handler emits Infallible, so this is unreachable + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> Poll>> { + if let Some(event) = self.pending_events.pop_front() { + return Poll::Ready(event); + } + + if self.ticker.poll_tick(cx).is_ready() { + self.run_upgrade_logic(); + + if let Some(event) = self.pending_events.pop_front() { + return Poll::Ready(event); + } + } + + Poll::Pending + } +} diff --git a/crates/p2p/src/utils.rs b/crates/p2p/src/utils.rs index 640dfb7c..4e81ae7c 100644 --- a/crates/p2p/src/utils.rs +++ b/crates/p2p/src/utils.rs @@ -166,3 +166,31 @@ pub fn is_quic_addr(addr: &Multiaddr) -> bool { pub fn is_tcp_addr(addr: &Multiaddr) -> bool { addr.iter().any(|p| matches!(p, MaProtocol::Tcp(_))) } + +/// Returns true if the node has QUIC enabled (listening on QUIC addresses). +pub fn is_quic_enabled<'a>(listen_addrs: impl Iterator) -> bool { + listen_addrs.into_iter().any(is_quic_addr) +} + +/// Returns true if there is a direct (non-relay) QUIC connection among the +/// peers. +pub fn has_direct_quic_conn(peers: &[&crate::p2p_context::Peer]) -> bool { + peers + .iter() + .any(|p| is_quic_addr(&p.remote_addr) && !is_relay_addr(&p.remote_addr)) +} + +/// Returns true if there is a direct (non-relay) TCP connection among the +/// peers. +pub fn has_direct_tcp_conn(peers: &[&crate::p2p_context::Peer]) -> bool { + peers + .iter() + .any(|p| is_tcp_addr(&p.remote_addr) && !is_relay_addr(&p.remote_addr)) +} + +/// Filters addresses to only direct (non-relay) QUIC addresses. +pub fn filter_direct_quic_addrs(addrs: impl Iterator) -> Vec { + addrs + .filter(|a| is_quic_addr(a) && !is_relay_addr(a)) + .collect() +} From 1dce1a714e06d18afa9acb94901c46aead97af24 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:38:46 +0100 Subject: [PATCH 23/29] feat(p2p): add force direct connections (#260) * feat: add quic upgrade * feat: add comment about peer addresses * fix: linter * fix: clean up tests * fix: linter * feat: add force direct connections * fix: linter * fix: review comments * fix: review comments * fix: review comments --- crates/p2p/src/force_direct.rs | 280 +++++++++++++++++++++++++++++++++ crates/p2p/src/lib.rs | 3 + crates/p2p/src/utils.rs | 5 + 3 files changed, 288 insertions(+) create mode 100644 crates/p2p/src/force_direct.rs diff --git a/crates/p2p/src/force_direct.rs b/crates/p2p/src/force_direct.rs new file mode 100644 index 00000000..b8c9f49c --- /dev/null +++ b/crates/p2p/src/force_direct.rs @@ -0,0 +1,280 @@ +//! Force direct connection behaviour. + +use std::{ + collections::{HashSet, VecDeque}, + convert::Infallible, + task::{Context, Poll}, +}; + +use libp2p::{ + Multiaddr, PeerId, + swarm::{ + ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, THandler, ToSwarm, + behaviour::ConnectionEstablished, + dial_opts::{DialOpts, PeerCondition}, + dummy, + }, +}; +use std::time::Duration; +use tokio::time::Interval; +use tracing::{debug, warn}; + +use crate::{name::peer_name, p2p_context::P2PContext, utils}; + +const FORCE_DIRECT_INTERVAL: Duration = Duration::from_secs(60); + +/// Force direct connection behaviour. +pub struct ForceDirectBehaviour { + /// P2P context for accessing peer store and known peers. + p2p_context: P2PContext, + + /// Local peer ID (to skip self). + local_peer_id: PeerId, + + /// Pending events to emit. + pending_events: VecDeque>, + + /// Pending forcings to emit. + pending_forcings: HashSet, + + /// Interval timer for running force direct logic periodically. + ticker: Interval, +} + +impl std::fmt::Debug for ForceDirectBehaviour { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ForceDirectBehaviour") + .field("p2p_context", &self.p2p_context) + .field("local_peer_id", &self.local_peer_id) + .field("pending_events", &self.pending_events.len()) + .field("ticker", &"") + .finish() + } +} + +/// Events emitted by the force direct behaviour. +#[derive(Debug, Clone)] +pub enum ForceDirectEvent { + /// Force direct connection to a peer. + ForceDirectSuccess { + /// The peer to force direct connection to. + peer: PeerId, + }, + /// Force direct connection failed. + ForceDirectFailure { + /// The peer to force direct connection to. + peer: PeerId, + /// The reason for the failure. + reason: String, + }, +} + +impl ForceDirectBehaviour { + /// Creates a new force direct behaviour. + pub fn new(p2p_context: P2PContext, local_peer_id: PeerId) -> Self { + let mut ticker = tokio::time::interval(FORCE_DIRECT_INTERVAL); + ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + Self { + p2p_context, + local_peer_id, + pending_events: VecDeque::new(), + ticker, + pending_forcings: HashSet::new(), + } + } + + /// Runs force direct connection logic for all known peers. + /// + /// For each known peer: + /// 1. Skip if it's the local peer + /// 2. Skip if already attempting to force direct connection + /// 3. Skip if no connections exist + /// 4. Skip if any connection is not through relay + /// 5. Attempt to dial direct addresses + fn force_direct_connections(&mut self) { + let peers = self.p2p_context.known_peers(); + + for peer in peers { + if *peer == self.local_peer_id { + continue; + } + + if self.pending_forcings.contains(peer) { + continue; + } + + let (connections, available_addresses): ( + Vec, + Option>, + ) = { + let lock = self.p2p_context.peer_store_lock(); + + ( + lock.connections_to_peer(peer) + .into_iter() + .cloned() + .collect::>(), + lock.peer_addresses(peer) + .cloned() + .map(|v| v.into_iter().collect()), + ) + }; + + if connections.is_empty() { + warn!( + peer = %peer_name(peer), + "no connections to peer" + ); + continue; + } + + if connections + .iter() + .any(|c| !utils::is_relay_addr(&c.remote_addr)) + { + debug!( + peer = %peer_name(peer), + "not all connections to peer are relay connections, skipping force direct" + ); + continue; + } + + let Some(addresses) = available_addresses else { + warn!( + peer = %peer_name(peer), + "no known addresses for peer" + ); + continue; + }; + + // Find non-relay addresses + let direct_addresses: Vec = addresses + .iter() + .filter(|addr| utils::is_direct_addr(addr)) + .cloned() + .collect(); + + if direct_addresses.is_empty() { + warn!( + peer = %peer_name(peer), + "no direct addresses for peer, cannot force direct connection" + ); + continue; + } + + debug!( + peer = %peer_name(peer), + direct_addresses = ?direct_addresses, + "forcing direct connection to peer using {} available addresses", + direct_addresses.len() + ); + + self.pending_forcings.insert(*peer); + + self.pending_events.push_back(ToSwarm::Dial { + opts: DialOpts::peer_id(*peer) + .addresses(direct_addresses) + .condition(PeerCondition::Always) + .build(), + }); + } + } + + fn handle_connection_established(&mut self, event: ConnectionEstablished) { + let addr = match &event.endpoint { + libp2p::core::ConnectedPoint::Dialer { address, .. } => address, + libp2p::core::ConnectedPoint::Listener { send_back_addr, .. } => send_back_addr, + }; + + if self.pending_forcings.contains(&event.peer_id) && utils::is_direct_addr(addr) { + self.pending_forcings.remove(&event.peer_id); + self.pending_events.push_back(ToSwarm::GenerateEvent( + ForceDirectEvent::ForceDirectSuccess { + peer: event.peer_id, + }, + )); + } + } + + fn handle_dial_failure(&mut self, peer_id: Option) { + let Some(peer_id) = peer_id else { + return; + }; + + if self.pending_forcings.remove(&peer_id) { + self.pending_events.push_back(ToSwarm::GenerateEvent( + ForceDirectEvent::ForceDirectFailure { + peer: peer_id, + reason: "dial failed".to_string(), + }, + )); + } + } +} + +impl NetworkBehaviour for ForceDirectBehaviour { + type ConnectionHandler = dummy::ConnectionHandler; + type ToSwarm = ForceDirectEvent; + + fn handle_established_inbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _local_addr: &Multiaddr, + _remote_addr: &Multiaddr, + ) -> Result, ConnectionDenied> { + Ok(dummy::ConnectionHandler) + } + + fn handle_established_outbound_connection( + &mut self, + _connection_id: ConnectionId, + _peer: PeerId, + _addr: &Multiaddr, + _role_override: libp2p::core::Endpoint, + _port_use: libp2p::core::transport::PortUse, + ) -> Result, ConnectionDenied> { + Ok(dummy::ConnectionHandler) + } + + fn on_swarm_event(&mut self, event: libp2p::swarm::FromSwarm) { + match event { + FromSwarm::ConnectionEstablished(event) => { + self.handle_connection_established(event); + } + FromSwarm::DialFailure(event) => { + self.handle_dial_failure(event.peer_id); + } + _ => {} + } + } + + fn on_connection_handler_event( + &mut self, + _peer_id: PeerId, + _connection_id: ConnectionId, + _event: libp2p::swarm::THandlerOutEvent, + ) { + // Handler emits Infallible, so this is unreachable + } + + fn poll( + &mut self, + cx: &mut Context<'_>, + ) -> std::task::Poll>> { + if let Some(event) = self.pending_events.pop_front() { + return Poll::Ready(event); + } + + if self.ticker.poll_tick(cx).is_ready() { + self.force_direct_connections(); + + if let Some(event) = self.pending_events.pop_front() { + return Poll::Ready(event); + } + } + + Poll::Pending + } +} diff --git a/crates/p2p/src/lib.rs b/crates/p2p/src/lib.rs index 3be87dbc..3d4afa25 100644 --- a/crates/p2p/src/lib.rs +++ b/crates/p2p/src/lib.rs @@ -43,3 +43,6 @@ pub mod p2p_context; /// QUIC connection upgrade behaviour. pub mod quic_upgrade; + +/// Force direct connection behaviour. +pub mod force_direct; diff --git a/crates/p2p/src/utils.rs b/crates/p2p/src/utils.rs index 4e81ae7c..8a1286f6 100644 --- a/crates/p2p/src/utils.rs +++ b/crates/p2p/src/utils.rs @@ -194,3 +194,8 @@ pub fn filter_direct_quic_addrs(addrs: impl Iterator) -> Vec bool { + !is_relay_addr(addr) +} From 234606b73811fe4215387eb25ffc263e78633fb4 Mon Sep 17 00:00:00 2001 From: Quang Le Date: Tue, 10 Mar 2026 18:29:29 +0700 Subject: [PATCH 24/29] fix: update quinn-proto package (#275) --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bf12011..56e9b94d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2234,7 +2234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.117", + "syn 1.0.109", ] [[package]] @@ -6090,9 +6090,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", From c073a44fd0cb9afc355a95bdea666856189fed8b Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 10 Mar 2026 10:21:40 -0300 Subject: [PATCH 25/29] Validate filename --- crates/app/src/utils.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 783baa11..eaec6d10 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -68,10 +68,14 @@ pub fn bundle_output( filename: impl AsRef, ) -> Result<()> { // Compute and validate the output path - let output_path = path::Path::new(target_path.as_ref()).join(filename.as_ref()); - if output_path.parent() != Some(target_path.as_ref()) { + if filename + .as_ref() + .components() + .any(|c| !matches!(c, path::Component::Normal(_))) + { return Err(UtilsError::IllegalFilename); } + let output_path = path::Path::new(target_path.as_ref()).join(filename.as_ref()); // Create output file let tar_file = tempfile::NamedTempFile::new()?; @@ -566,6 +570,16 @@ mod tests { assert!(result.is_ok()); } + #[test_case("../file.tar.gz"; "relative path")] + #[test_case("/absolute/path/file.tar.gz"; "absolute path")] + #[test_case(".."; "invalid name")] + fn bundle_output_invalid_filenames(filename: &str) { + let target_dir = tempfile::tempdir().unwrap(); + let result = super::bundle_output(target_dir.path(), filename); + + assert!(matches!(result, Err(super::UtilsError::IllegalFilename))); + } + /// Recursively copies all files and directories from `from` to `to`. fn copy_dir_all(from: impl AsRef, to: impl AsRef) -> io::Result<()> { fs::create_dir_all(&to)?; // Create the destination directory and all its parents From 0f81a9d0c72f83d0e74d57aeb740934287bd057f Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Thu, 12 Mar 2026 11:30:22 -0300 Subject: [PATCH 26/29] Update `Cargo.lock` --- Cargo.lock | 311 +++++++++++++++++++++++++++-------------------------- 1 file changed, 156 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 56e9b94d..26345620 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,9 +104,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.2.30" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f374d3c6d729268bbe2d0e0ff992bb97898b2df756691a62ee1d5f0506bc39" +checksum = "6d9d22005bf31b018f31ef9ecadb5d2c39cf4f6acc8db0456f72c815f3d7f757" dependencies = [ "alloy-primitives", "num_enum", @@ -245,9 +245,9 @@ dependencies = [ [[package]] name = "alloy-eip7928" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3231de68d5d6e75332b7489cfcc7f4dfabeba94d990a10e4b923af0e6623540" +checksum = "f8222b1d88f9a6d03be84b0f5e76bb60cd83991b43ad8ab6477f0e4a7809b98d" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -671,13 +671,12 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d7fd448ab0a017de542de1dcca7a58e7019fe0e7a34ed3f9543ebddf6aceffa" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" dependencies = [ "alloy-primitives", "alloy-rlp", - "arrayvec", "derive_more", "nybbles", "serde", @@ -969,9 +968,6 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -dependencies = [ - "serde", -] [[package]] name = "asn1-rs" @@ -1157,9 +1153,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" dependencies = [ "aws-lc-sys", "zeroize", @@ -1167,9 +1163,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" dependencies = [ "cc", "cmake", @@ -1328,12 +1324,6 @@ dependencies = [ "hex-conservative", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.11.0" @@ -1390,7 +1380,7 @@ checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b" dependencies = [ "async-stream", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags", "bollard-buildkit-proto", "bollard-stubs", "bytes", @@ -1556,9 +1546,9 @@ dependencies = [ [[package]] name = "c-kzg" -version = "2.1.6" +version = "2.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0f582957c24870b7bfd12bf562c40b4734b533cafbaf8ded31d6d85f462c01" +checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" dependencies = [ "blst", "cc", @@ -2234,7 +2224,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -2946,20 +2936,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -2980,7 +2970,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.11.0", + "bitflags", "libc", "libgit2-sys", "log", @@ -3378,8 +3368,8 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.2", - "system-configuration 0.7.0", + "socket2 0.6.3", + "system-configuration", "tokio", "tower-service", "tracing", @@ -3541,19 +3531,19 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.10.2" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabb0019d51a643781ff15c9c8a3e5dedc365c47211270f4e8f82812fedd8f0a" +checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] name = "if-watch" -version = "3.2.1" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf9d64cfcf380606e64f9a0bcf493616b65331199f984151a6fa11a7b3cde38" +checksum = "71c02a5161c313f0cbdbadc511611893584a10a7b6153cb554bdf83ddce99ec2" dependencies = [ "async-io", "core-foundation 0.9.4", @@ -3567,9 +3557,9 @@ dependencies = [ "netlink-proto", "netlink-sys", "rtnetlink", - "system-configuration 0.6.1", + "system-configuration", "tokio", - "windows 0.53.0", + "windows 0.62.2", ] [[package]] @@ -3659,9 +3649,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -3802,9 +3792,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libgit2-sys" @@ -4007,9 +3997,9 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" -version = "0.49.2" +version = "0.49.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f58e37d8d6848e5c4c9e3c35c6f61133235bff2960c9c00a663b0849301221" +checksum = "4cef64c3bdfaee9561319a289d778e9f8c56bd8e10f5d1059289ebb085ef09d7" dependencies = [ "async-channel", "asynchronous-codec", @@ -4367,7 +4357,7 @@ dependencies = [ "if-watch", "libc", "libp2p-core", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", "tracing", ] @@ -4532,7 +4522,7 @@ dependencies = [ "thiserror 2.0.18", "tracing", "yamux 0.12.1", - "yamux 0.13.9", + "yamux 0.13.10", ] [[package]] @@ -4541,7 +4531,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.11.0", + "bitflags", "libc", "plain", "redox_syscall 0.7.3", @@ -4549,9 +4539,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.24" +version = "1.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" dependencies = [ "cc", "libc", @@ -4805,46 +4795,30 @@ dependencies = [ [[package]] name = "netlink-packet-core" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" dependencies = [ - "anyhow", - "byteorder", - "netlink-packet-utils", + "paste", ] [[package]] name = "netlink-packet-route" -version = "0.17.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" dependencies = [ - "anyhow", - "bitflags 1.3.2", - "byteorder", + "bitflags", "libc", + "log", "netlink-packet-core", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-utils" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" -dependencies = [ - "anyhow", - "byteorder", - "paste", - "thiserror 1.0.69", ] [[package]] name = "netlink-proto" -version = "0.11.5" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" dependencies = [ "bytes", "futures", @@ -4869,12 +4843,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.4" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 1.3.2", + "bitflags", "cfg-if", + "cfg_aliases", "libc", ] @@ -5097,9 +5072,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" dependencies = [ "critical-section", "portable-atomic", @@ -5125,11 +5100,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -5157,9 +5132,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -5866,11 +5841,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.4+spec-1.1.0", ] [[package]] @@ -5935,7 +5910,7 @@ checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.11.0", + "bitflags", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -6081,7 +6056,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.2", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -6119,16 +6094,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -6139,6 +6114,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -6305,7 +6286,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] @@ -6314,7 +6295,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] @@ -6494,15 +6475,15 @@ dependencies = [ [[package]] name = "rtnetlink" -version = "0.13.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0" +checksum = "4b960d5d873a75b5be9761b1e73b146f52dddcd27bac75263f40fba686d4d7b5" dependencies = [ - "futures", + "futures-channel", + "futures-util", "log", "netlink-packet-core", "netlink-packet-route", - "netlink-packet-utils", "netlink-proto", "netlink-sys", "nix", @@ -6589,7 +6570,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -6737,9 +6718,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -6828,7 +6809,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", + "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -7162,12 +7143,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7331,24 +7312,13 @@ dependencies = [ "windows 0.57.0", ] -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - [[package]] name = "system-configuration" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.0", + "bitflags", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -7388,12 +7358,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -7588,9 +7558,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -7598,7 +7568,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -7682,9 +7652,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] @@ -7705,12 +7675,12 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap 2.13.0", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "winnow", ] @@ -7749,7 +7719,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2 0.6.2", + "socket2 0.6.3", "sync_wrapper", "tokio", "tokio-stream", @@ -7795,7 +7765,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.11.0", + "bitflags", "bytes", "futures-util", "http", @@ -8116,11 +8086,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -8369,7 +8339,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags", "hashbrown 0.15.5", "indexmap 2.13.0", "semver 1.0.27", @@ -8475,32 +8445,33 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.53.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc5cf48f83140dcaab716eeaea345f9e93d0018fb81162753a3f76c3397b538" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ - "windows-core 0.53.0", + "windows-core 0.57.0", "windows-targets 0.52.6", ] [[package]] name = "windows" -version = "0.57.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", ] [[package]] -name = "windows-core" -version = "0.53.0" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcc5b895a6377f1ab9fa55acedab1fd5ac0db66ad1e6c7f47e28a22e446a5dd" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", + "windows-core 0.62.2", ] [[package]] @@ -8528,6 +8499,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.57.0" @@ -8578,6 +8560,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -8724,6 +8716,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -8906,9 +8907,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -9004,7 +9005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags", "indexmap 2.13.0", "log", "serde", @@ -9120,9 +9121,9 @@ dependencies = [ [[package]] name = "yamux" -version = "0.13.9" +version = "0.13.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c650efd29044140aa63caaf80129996a9e2659a2ab7045a7e061807d02fc8549" +checksum = "1991f6690292030e31b0144d73f5e8368936c58e45e7068254f7138b23b00672" dependencies = [ "futures", "log", @@ -9168,18 +9169,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", From 6a0b27bd194e7b0b8cdf35871412d00be2352fa2 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 13:26:10 -0300 Subject: [PATCH 27/29] Prefer `&Path` to `&PathBuf` --- crates/app/src/utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index eaec6d10..4f9f159c 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -174,10 +174,10 @@ pub fn compare_directories( } /// Compare two files for equality. -fn compare_file_contents(path1: &path::PathBuf, path2: &path::PathBuf) -> Result<()> { +fn compare_file_contents(path1: &path::Path, path2: &path::Path) -> Result<()> { let error = Err(UtilsError::ContentMismatch { - expected: path1.clone(), - found: path2.clone(), + expected: path1.to_path_buf(), + found: path2.to_path_buf(), }); // Fast path: compare metadata first From 0f4787f17231e6871ce3e25b923f3c7ccbbb3c54 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 13:26:32 -0300 Subject: [PATCH 28/29] Fix `Streaming file comparison is not read-safe` --- crates/app/src/utils.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 4f9f159c..d59b0ce6 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -207,15 +207,14 @@ fn compare_file_contents(path1: &path::Path, path2: &path::Path) -> Result<()> { let mut buf2 = [0u8; BUFFER_SIZE]; loop { - let n1 = file1.read(&mut buf1)?; - let n2 = file2.read(&mut buf2)?; - - if n1 != n2 || buf1[..n1] != buf2[..n2] { - return error; + let n = file1.read(&mut buf1)?; + if n == 0 { + break; } - - if n1 == 0 { - break; // EOF + // `read_exact` is safe here because sizes are equal and we haven't reached EOF + file2.read_exact(&mut buf2[..n])?; + if buf1[..n] != buf2[..n] { + return error; } } } From bd857438f9c012b8b3ef162dcb79c22914f3bca9 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Fri, 13 Mar 2026 13:29:14 -0300 Subject: [PATCH 29/29] Remove dev-dependency `tempfile` - Duplicated --- crates/app/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index c65f5bc8..d799fd7f 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -39,7 +39,6 @@ pluto-crypto.workspace = true pluto-build-proto.workspace = true [dev-dependencies] -tempfile.workspace = true wiremock.workspace = true test-case.workspace = true