diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index f306759803..564de2707e 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -127,6 +127,10 @@ jobs: timeout-minutes: 25 run: cargo test --release --package autonomi --lib --features="full,fs" + - name: Run bootstrap tests + timeout-minutes: 25 + run: cargo test --release --package ant-bootstrap + - name: Run node tests timeout-minutes: 25 run: cargo test --release --package ant-node --lib diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 32870fff79..23a9b78f99 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -244,6 +244,14 @@ jobs: run: cargo test --release --lib --bins --no-run timeout-minutes: 30 + - name: Run autonomi tests + timeout-minutes: 25 + run: cargo test --release --package autonomi --lib --features="full,fs" + + - name: Run bootstrap tests + timeout-minutes: 25 + run: cargo test --release --package ant-bootstrap + - name: Run node tests timeout-minutes: 25 run: cargo test --release --package ant-node --lib diff --git a/.gitignore b/.gitignore index ef12210b4e..3d525ed581 100644 --- a/.gitignore +++ b/.gitignore @@ -36,8 +36,7 @@ ant-node-manager/.vagrant .venv/ uv.lock *.so -*.pyc - *.pyc *.swp +/vendor/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index aff7d76738..34ae07c699 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -722,6 +722,31 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ant-bootstrap" +version = "0.1.0" +dependencies = [ + "ant-logging", + "ant-protocol", + "atomic-write-file", + "chrono", + "clap", + "dirs-next", + "futures", + "libp2p 0.54.1 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.12.9", + "serde", + "serde_json", + "tempfile", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-subscriber", + "url", + "wasmtimer", + "wiremock", +] + [[package]] name = "ant-build-info" version = "0.1.19" @@ -735,9 +760,9 @@ dependencies = [ name = "ant-cli" version = "0.1.5" dependencies = [ + "ant-bootstrap", "ant-build-info", "ant-logging", - "ant-peers-acquisition", "autonomi", "clap", "color-eyre", @@ -769,7 +794,7 @@ dependencies = [ "evmlib", "hex 0.4.3", "lazy_static", - "libp2p", + "libp2p 0.54.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "rand 0.8.5", "ring 0.17.8", "rmp-serde", @@ -828,6 +853,7 @@ name = "ant-networking" version = "0.19.5" dependencies = [ "aes-gcm-siv", + "ant-bootstrap", "ant-build-info", "ant-evm", "ant-protocol", @@ -846,7 +872,7 @@ dependencies = [ "hyper 0.14.31", "itertools 0.12.1", "lazy_static", - "libp2p", + "libp2p 0.54.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "prometheus-client", "quickcheck", @@ -874,11 +900,11 @@ dependencies = [ name = "ant-node" version = "0.112.6" dependencies = [ + "ant-bootstrap", "ant-build-info", "ant-evm", "ant-logging", "ant-networking", - "ant-peers-acquisition", "ant-protocol", "ant-registers", "ant-service-management", @@ -900,7 +926,7 @@ dependencies = [ "futures", "hex 0.4.3", "itertools 0.12.1", - "libp2p", + "libp2p 0.54.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "num-traits", "prometheus-client", "prost 0.9.0", @@ -932,10 +958,10 @@ dependencies = [ name = "ant-node-manager" version = "0.11.3" dependencies = [ + "ant-bootstrap", "ant-build-info", "ant-evm", "ant-logging", - "ant-peers-acquisition", "ant-protocol", "ant-releases", "ant-service-management", @@ -949,7 +975,7 @@ dependencies = [ "colored", "dirs-next", "indicatif", - "libp2p", + "libp2p 0.54.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "mockall 0.12.1", "nix 0.27.1", @@ -978,7 +1004,6 @@ dependencies = [ "ant-build-info", "ant-logging", "ant-node", - "ant-peers-acquisition", "ant-protocol", "ant-service-management", "async-trait", @@ -986,7 +1011,7 @@ dependencies = [ "clap", "color-eyre", "hex 0.4.3", - "libp2p", + "libp2p 0.54.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "thiserror 1.0.69", "tokio", @@ -996,22 +1021,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "ant-peers-acquisition" -version = "0.5.7" -dependencies = [ - "ant-protocol", - "clap", - "lazy_static", - "libp2p", - "rand 0.8.5", - "reqwest 0.12.9", - "thiserror 1.0.69", - "tokio", - "tracing", - "url", -] - [[package]] name = "ant-protocol" version = "0.17.15" @@ -1028,7 +1037,7 @@ dependencies = [ "exponential-backoff", "hex 0.4.3", "lazy_static", - "libp2p", + "libp2p 0.54.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "prost 0.9.0", "rmp-serde", "serde", @@ -1087,7 +1096,7 @@ dependencies = [ "ant-protocol", "async-trait", "dirs-next", - "libp2p", + "libp2p 0.54.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "mockall 0.11.4", "prost 0.9.0", @@ -1333,6 +1342,16 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "assert_cmd" version = "2.0.16" @@ -1370,6 +1389,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + [[package]] name = "async-io" version = "2.4.0" @@ -1380,7 +1410,7 @@ dependencies = [ "cfg-if", "concurrent-queue", "futures-io", - "futures-lite", + "futures-lite 2.5.0", "parking", "polling", "rustix", @@ -1395,7 +1425,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener", + "event-listener 5.3.1", "event-listener-strategy", "pin-project-lite", ] @@ -1446,6 +1476,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "atomic-write-file" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e32862ecc63d580f4a5e1436a685f51e0629caeb7a7933e4f017d5e2099e13" +dependencies = [ + "nix 0.29.0", + "rand 0.8.5", +] + [[package]] name = "attohttpc" version = "0.24.1" @@ -1499,10 +1539,10 @@ name = "autonomi" version = "0.2.4" dependencies = [ "alloy", + "ant-bootstrap", "ant-evm", "ant-logging", "ant-networking", - "ant-peers-acquisition", "ant-protocol", "ant-registers", "bip39", @@ -1518,7 +1558,7 @@ dependencies = [ "hex 0.4.3", "instant", "js-sys", - "libp2p", + "libp2p 0.54.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "pyo3", "rand 0.8.5", "rmp-serde", @@ -2817,6 +2857,25 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deadpool" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "retain_mut", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "der" version = "0.6.1" @@ -3213,6 +3272,12 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "event-listener" version = "5.3.1" @@ -3230,7 +3295,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ - "event-listener", + "event-listener 5.3.1", "pin-project-lite", ] @@ -3266,7 +3331,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ffb309d235a642598183aeda8925e871e85dd5a433c2c877e69ff0a960f4c02" dependencies = [ - "fastrand", + "fastrand 2.2.0", ] [[package]] @@ -3302,6 +3367,15 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.2.0" @@ -3572,6 +3646,21 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-lite" version = "2.5.0" @@ -3918,7 +4007,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2bfe6249cfea6d0c0e0990d5226a4cb36f030444ba9e35e0639275db8f98575" dependencies = [ - "fastrand", + "fastrand 2.2.0", "gix-features", "gix-utils", ] @@ -4224,7 +4313,7 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba427e3e9599508ed98a6ddf8ed05493db114564e338e41f6a996d2e4790335f" dependencies = [ - "fastrand", + "fastrand 2.2.0", "unicode-normalization", ] @@ -4627,6 +4716,27 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel", + "base64 0.13.1", + "futures-lite 1.13.0", + "http 0.2.12", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "url", +] + [[package]] name = "httparse" version = "1.9.5" @@ -5088,6 +5198,12 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + [[package]] name = "inout" version = "0.1.3" @@ -5278,6 +5394,30 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +[[package]] +name = "libp2p" +version = "0.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbe80f9c7e00526cd6b838075b9c171919404a4732cb2fa8ece0a093223bfc4" +dependencies = [ + "bytes", + "either", + "futures", + "futures-timer", + "getrandom 0.2.15", + "libp2p-allow-block-list 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-connection-limits 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-core 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-gossipsub 0.47.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-identity", + "libp2p-kad 0.46.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-swarm 0.45.1 (registry+https://github.com/rust-lang/crates.io-index)", + "multiaddr", + "pin-project", + "rw-stream-sink 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 1.0.69", +] + [[package]] name = "libp2p" version = "0.54.1" @@ -5288,22 +5428,22 @@ dependencies = [ "futures", "futures-timer", "getrandom 0.2.15", - "libp2p-allow-block-list", + "libp2p-allow-block-list 0.4.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-autonat", - "libp2p-connection-limits", - "libp2p-core", + "libp2p-connection-limits 0.4.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-dns", - "libp2p-gossipsub", + "libp2p-gossipsub 0.47.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identify", "libp2p-identity", - "libp2p-kad", + "libp2p-kad 0.46.2 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-mdns", "libp2p-metrics", "libp2p-noise", "libp2p-quic", "libp2p-relay", "libp2p-request-response", - "libp2p-swarm", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-tcp", "libp2p-upnp", "libp2p-websocket", @@ -5311,18 +5451,30 @@ dependencies = [ "libp2p-yamux", "multiaddr", "pin-project", - "rw-stream-sink", + "rw-stream-sink 0.4.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "thiserror 1.0.69", ] +[[package]] +name = "libp2p-allow-block-list" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1027ccf8d70320ed77e984f273bc8ce952f623762cb9bf2d126df73caef8041" +dependencies = [ + "libp2p-core 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-identity", + "libp2p-swarm 0.45.1 (registry+https://github.com/rust-lang/crates.io-index)", + "void", +] + [[package]] name = "libp2p-allow-block-list" version = "0.4.0" source = "git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2#15f0535f87256ff141963006af129cc2c839b472" dependencies = [ - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "void", ] @@ -5338,12 +5490,12 @@ dependencies = [ "futures", "futures-bounded", "futures-timer", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "libp2p-request-response", - "libp2p-swarm", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "quick-protobuf", - "quick-protobuf-codec", + "quick-protobuf-codec 0.3.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "rand 0.8.5", "rand_core 0.6.4", "thiserror 1.0.69", @@ -5352,17 +5504,58 @@ dependencies = [ "web-time", ] +[[package]] +name = "libp2p-connection-limits" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d003540ee8baef0d254f7b6bfd79bac3ddf774662ca0abf69186d517ef82ad8" +dependencies = [ + "libp2p-core 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-identity", + "libp2p-swarm 0.45.1 (registry+https://github.com/rust-lang/crates.io-index)", + "void", +] + [[package]] name = "libp2p-connection-limits" version = "0.4.0" source = "git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2#15f0535f87256ff141963006af129cc2c839b472" dependencies = [ - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "void", ] +[[package]] +name = "libp2p-core" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a61f26c83ed111104cd820fe9bc3aaabbac5f1652a1d213ed6e900b7918a1298" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "libp2p-identity", + "multiaddr", + "multihash", + "multistream-select 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "once_cell", + "parking_lot", + "pin-project", + "quick-protobuf", + "rand 0.8.5", + "rw-stream-sink 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", + "smallvec", + "thiserror 1.0.69", + "tracing", + "unsigned-varint 0.8.0", + "void", + "web-time", +] + [[package]] name = "libp2p-core" version = "0.42.0" @@ -5375,17 +5568,17 @@ dependencies = [ "libp2p-identity", "multiaddr", "multihash", - "multistream-select", + "multistream-select 0.13.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "once_cell", "parking_lot", "pin-project", "quick-protobuf", "rand 0.8.5", - "rw-stream-sink", + "rw-stream-sink 0.4.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "smallvec", "thiserror 1.0.69", "tracing", - "unsigned-varint", + "unsigned-varint 0.8.0", "void", "web-time", ] @@ -5398,13 +5591,45 @@ dependencies = [ "async-trait", "futures", "hickory-resolver", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "parking_lot", "smallvec", "tracing", ] +[[package]] +name = "libp2p-gossipsub" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4e830fdf24ac8c444c12415903174d506e1e077fbe3875c404a78c5935a8543" +dependencies = [ + "asynchronous-codec", + "base64 0.22.1", + "byteorder", + "bytes", + "either", + "fnv", + "futures", + "futures-ticker", + "getrandom 0.2.15", + "hex_fmt", + "libp2p-core 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-identity", + "libp2p-swarm 0.45.1 (registry+https://github.com/rust-lang/crates.io-index)", + "prometheus-client", + "quick-protobuf", + "quick-protobuf-codec 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.8.5", + "regex", + "serde", + "sha2 0.10.8", + "smallvec", + "tracing", + "void", + "web-time", +] + [[package]] name = "libp2p-gossipsub" version = "0.47.0" @@ -5420,12 +5645,12 @@ dependencies = [ "futures-ticker", "getrandom 0.2.15", "hex_fmt", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "prometheus-client", "quick-protobuf", - "quick-protobuf-codec", + "quick-protobuf-codec 0.3.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "rand 0.8.5", "regex", "sha2 0.10.8", @@ -5445,12 +5670,12 @@ dependencies = [ "futures", "futures-bounded", "futures-timer", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "lru", "quick-protobuf", - "quick-protobuf-codec", + "quick-protobuf-codec 0.3.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "smallvec", "thiserror 1.0.69", "tracing", @@ -5469,12 +5694,43 @@ dependencies = [ "multihash", "quick-protobuf", "rand 0.8.5", + "serde", "sha2 0.10.8", "thiserror 1.0.69", "tracing", "zeroize", ] +[[package]] +name = "libp2p-kad" +version = "0.46.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced237d0bd84bbebb7c2cad4c073160dacb4fe40534963c32ed6d4c6bb7702a3" +dependencies = [ + "arrayvec", + "asynchronous-codec", + "bytes", + "either", + "fnv", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-identity", + "libp2p-swarm 0.45.1 (registry+https://github.com/rust-lang/crates.io-index)", + "quick-protobuf", + "quick-protobuf-codec 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.8.5", + "serde", + "sha2 0.10.8", + "smallvec", + "thiserror 1.0.69", + "tracing", + "uint", + "void", + "web-time", +] + [[package]] name = "libp2p-kad" version = "0.46.2" @@ -5488,11 +5744,11 @@ dependencies = [ "futures", "futures-bounded", "futures-timer", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "quick-protobuf", - "quick-protobuf-codec", + "quick-protobuf-codec 0.3.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "rand 0.8.5", "sha2 0.10.8", "smallvec", @@ -5512,9 +5768,9 @@ dependencies = [ "futures", "hickory-proto", "if-watch", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "rand 0.8.5", "smallvec", "socket2", @@ -5529,12 +5785,12 @@ version = "0.15.0" source = "git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2#15f0535f87256ff141963006af129cc2c839b472" dependencies = [ "futures", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identify", "libp2p-identity", - "libp2p-kad", + "libp2p-kad 0.46.2 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-relay", - "libp2p-swarm", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "pin-project", "prometheus-client", "web-time", @@ -5549,7 +5805,7 @@ dependencies = [ "bytes", "curve25519-dalek 4.1.3", "futures", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "multiaddr", "multihash", @@ -5574,7 +5830,7 @@ dependencies = [ "futures", "futures-timer", "if-watch", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "libp2p-tls", "parking_lot", @@ -5599,11 +5855,11 @@ dependencies = [ "futures", "futures-bounded", "futures-timer", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "quick-protobuf", - "quick-protobuf-codec", + "quick-protobuf-codec 0.3.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "rand 0.8.5", "static_assertions", "thiserror 1.0.69", @@ -5622,9 +5878,9 @@ dependencies = [ "futures", "futures-bounded", "futures-timer", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "rand 0.8.5", "serde", "smallvec", @@ -5633,6 +5889,28 @@ dependencies = [ "web-time", ] +[[package]] +name = "libp2p-swarm" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dd6741793d2c1fb2088f67f82cf07261f25272ebe3c0b0c311e0c6b50e851a" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "libp2p-core 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-identity", + "lru", + "multistream-select 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)", + "once_cell", + "rand 0.8.5", + "smallvec", + "tracing", + "void", + "web-time", +] + [[package]] name = "libp2p-swarm" version = "0.45.1" @@ -5643,11 +5921,11 @@ dependencies = [ "futures", "futures-timer", "getrandom 0.2.15", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "libp2p-swarm-derive", "lru", - "multistream-select", + "multistream-select 0.13.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "once_cell", "rand 0.8.5", "smallvec", @@ -5678,7 +5956,7 @@ dependencies = [ "futures-timer", "if-watch", "libc", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "socket2", "tokio", @@ -5692,7 +5970,7 @@ source = "git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2#15f0535f dependencies = [ "futures", "futures-rustls", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "rcgen", "ring 0.17.8", @@ -5711,8 +5989,8 @@ dependencies = [ "futures", "futures-timer", "igd-next", - "libp2p-core", - "libp2p-swarm", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", + "libp2p-swarm 0.45.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "tokio", "tracing", "void", @@ -5726,11 +6004,11 @@ dependencies = [ "either", "futures", "futures-rustls", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "libp2p-identity", "parking_lot", "pin-project-lite", - "rw-stream-sink", + "rw-stream-sink 0.4.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "soketto", "thiserror 1.0.69", "tracing", @@ -5746,7 +6024,7 @@ dependencies = [ "bytes", "futures", "js-sys", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "parking_lot", "send_wrapper 0.6.0", "thiserror 1.0.69", @@ -5762,7 +6040,7 @@ source = "git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2#15f0535f dependencies = [ "either", "futures", - "libp2p-core", + "libp2p-core 0.42.0 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "thiserror 1.0.69", "tracing", "yamux 0.12.1", @@ -6048,7 +6326,7 @@ dependencies = [ "percent-encoding", "serde", "static_assertions", - "unsigned-varint", + "unsigned-varint 0.8.0", "url", ] @@ -6070,7 +6348,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc41f430805af9d1cf4adae4ed2149c759b877b01d909a1f40256188d09345d2" dependencies = [ "core2", - "unsigned-varint", + "serde", + "unsigned-varint 0.8.0", ] [[package]] @@ -6079,6 +6358,20 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "multistream-select" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project", + "smallvec", + "unsigned-varint 0.7.2", +] + [[package]] name = "multistream-select" version = "0.13.0" @@ -6089,7 +6382,7 @@ dependencies = [ "pin-project", "smallvec", "tracing", - "unsigned-varint", + "unsigned-varint 0.8.0", ] [[package]] @@ -6103,7 +6396,7 @@ dependencies = [ "clap-verbosity-flag", "color-eyre", "futures", - "libp2p", + "libp2p 0.54.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "tokio", "tracing", "tracing-log 0.2.0", @@ -6197,14 +6490,26 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "node-launchpad" version = "0.4.5" dependencies = [ + "ant-bootstrap", "ant-build-info", "ant-evm", "ant-node-manager", - "ant-peers-acquisition", "ant-protocol", "ant-releases", "ant-service-management", @@ -7371,6 +7676,19 @@ dependencies = [ "byteorder", ] +[[package]] +name = "quick-protobuf-codec" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" +dependencies = [ + "asynchronous-codec", + "bytes", + "quick-protobuf", + "thiserror 1.0.69", + "unsigned-varint 0.8.0", +] + [[package]] name = "quick-protobuf-codec" version = "0.3.1" @@ -7380,7 +7698,7 @@ dependencies = [ "bytes", "quick-protobuf", "thiserror 1.0.69", - "unsigned-varint", + "unsigned-varint 0.8.0", ] [[package]] @@ -7900,6 +8218,12 @@ dependencies = [ "quick-error", ] +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + [[package]] name = "rfc6979" version = "0.3.1" @@ -8237,6 +8561,17 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rw-stream-sink" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" +dependencies = [ + "futures", + "pin-project", + "static_assertions", +] + [[package]] name = "rw-stream-sink" version = "0.4.0" @@ -8470,6 +8805,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -9049,7 +9395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", - "fastrand", + "fastrand 2.2.0", "once_cell", "rustix", "windows-sys 0.59.0", @@ -9086,12 +9432,11 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" name = "test-utils" version = "0.4.11" dependencies = [ - "ant-peers-acquisition", "bytes", "color-eyre", "dirs-next", "evmlib", - "libp2p", + "libp2p 0.54.1 (git+https://github.com/maqi/rust-libp2p.git?branch=kad_0.46.2)", "rand 0.8.5", "serde", "serde_json", @@ -9864,6 +10209,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + [[package]] name = "unsigned-varint" version = "0.8.0" @@ -9891,6 +10242,7 @@ dependencies = [ "form_urlencoded", "idna 1.0.3", "percent-encoding", + "serde", ] [[package]] @@ -10004,6 +10356,12 @@ dependencies = [ "libc", ] +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "walkdir" version = "2.5.0" @@ -10536,6 +10894,28 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "wiremock" +version = "0.5.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a3a53eaf34f390dd30d7b1b078287dd05df2aa2e21a589ccb80f5c7253c2e9" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.21.7", + "deadpool", + "futures", + "futures-timer", + "http-types", + "hyper 0.14.31", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 175e0dfa2c..6840a1e40d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "ant-bootstrap", "ant-build-info", "ant-cli", "ant-evm", @@ -10,7 +11,6 @@ members = [ "ant-node", "ant-node-manager", "ant-node-rpc-client", - "ant-peers-acquisition", "ant-protocol", "ant-registers", "ant-service-management", diff --git a/Justfile b/Justfile index c80fcf1b1a..2eb3768d03 100644 --- a/Justfile +++ b/Justfile @@ -68,16 +68,16 @@ build-release-artifacts arch nightly="false": cargo binstall --no-confirm cross cross build --release --target $arch --bin nat-detection $nightly_feature cross build --release --target $arch --bin node-launchpad $nightly_feature - cross build --release --features network-contacts,websockets --target $arch --bin ant $nightly_feature - cross build --release --features network-contacts,websockets --target $arch --bin antnode $nightly_feature + cross build --release --features websockets --target $arch --bin ant $nightly_feature + cross build --release --features websockets --target $arch --bin antnode $nightly_feature cross build --release --target $arch --bin antctl $nightly_feature cross build --release --target $arch --bin antctld $nightly_feature cross build --release --target $arch --bin antnode_rpc_client $nightly_feature else cargo build --release --target $arch --bin nat-detection $nightly_feature cargo build --release --target $arch --bin node-launchpad $nightly_feature - cargo build --release --features network-contacts,websockets --target $arch --bin ant $nightly_feature - cargo build --release --features network-contacts,websockets --target $arch --bin antnode $nightly_feature + cargo build --release --features websockets --target $arch --bin ant $nightly_feature + cargo build --release --features websockets --target $arch --bin antnode $nightly_feature cargo build --release --target $arch --bin antctl $nightly_feature cargo build --release --target $arch --bin antctld $nightly_feature cargo build --release --target $arch --bin antnode_rpc_client $nightly_feature diff --git a/README.md b/README.md index 014ea96496..bac5d08181 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ You should build from the `stable` branch, as follows: ``` git checkout stable -cargo build --release --features network-contacts --bin antnode +cargo build --release --bin antnode ``` #### Running the Node @@ -40,23 +40,12 @@ cargo build --release --features network-contacts --bin antnode To run a node and receive rewards, you need to specify your Ethereum address as a parameter. Rewards are paid to the specified address. ``` -cargo run --release --bin antnode --features network-contacts -- --rewards-address +cargo run --release --bin antnode -- --rewards-address ``` More options about EVM Network below. ### For Developers - -#### Build - -You can build `autonomi` and `antnode` with the `network-contacts` feature: - -``` -cargo build --release --features network-contacts --bin autonomi -cargo build --release --features network-contacts --bin antnode -``` - - #### Main Crates - [Autonomi API](https://github.com/maidsafe/autonomi/blob/main/autonomi/README.md) The client APIs @@ -97,8 +86,8 @@ WASM support for the autonomi API is currently under active development. More do used by the autonomi network. - [Registers](https://github.com/maidsafe/autonomi/blob/main/ant-registers/README.md) The registers crate, used for the Register CRDT data type on the network. -- [Peers Acquisition](https://github.com/maidsafe/autonomi/blob/main/ant-peers-acquisition/README.md) - The peers acquisition crate, or: how the network layer discovers bootstrap peers. +- [Bootstrap](https://github.com/maidsafe/autonomi/blob/main/ant-bootstrap/README.md) + The network bootstrap cache or: how the network layer discovers bootstrap peers. - [Build Info](https://github.com/maidsafe/autonomi/blob/main/ant-build-info/README.md) Small helper used to get the build/commit versioning info for debug purposes. diff --git a/ant-bootstrap/Cargo.toml b/ant-bootstrap/Cargo.toml new file mode 100644 index 0000000000..1e292cd64d --- /dev/null +++ b/ant-bootstrap/Cargo.toml @@ -0,0 +1,41 @@ +[package] +authors = ["MaidSafe Developers "] +description = "Bootstrap functionality for Autonomi" +edition = "2021" +homepage = "https://maidsafe.net" +license = "GPL-3.0" +name = "ant-bootstrap" +readme = "README.md" +repository = "https://github.com/maidsafe/autonomi" +version = "0.1.0" + +[features] +local = [] + +[dependencies] +ant-logging = { path = "../ant-logging", version = "0.2.40" } +ant-protocol = { version = "0.17.15", path = "../ant-protocol" } +atomic-write-file = "0.2.2" +chrono = { version = "0.4", features = ["serde"] } +clap = { version = "4.2.1", features = ["derive", "env"] } +dirs-next = "~2.0.0" +futures = "0.3.30" +libp2p = { version = "0.54.1", features = ["serde"] } +reqwest = { version = "0.12.2", default-features = false, features = [ + "rustls-tls-manual-roots", +] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +tokio = { version = "1.0", features = ["time"] } +tracing = "0.1" +url = "2.4.0" + +[dev-dependencies] +wiremock = "0.5" +tokio = { version = "1.0", features = ["full", "test-util"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tempfile = "3.8.1" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasmtimer = "0.2.0" \ No newline at end of file diff --git a/ant-bootstrap/README.md b/ant-bootstrap/README.md new file mode 100644 index 0000000000..35184cdbfb --- /dev/null +++ b/ant-bootstrap/README.md @@ -0,0 +1,21 @@ +# Bootstrap Cache + +A robust peer caching system for the Autonomi Network that provides persistent storage and management of network peer addresses. This crate handles peer discovery, caching, and reliability tracking with support for concurrent access across multiple processes. + +## Features + +### Storage and Accessibility +- System-wide accessible cache location +- Configurable primary cache location +- Cross-process safe with file locking +- Atomic write operations to prevent cache corruption + +### Data Management +- Automatic cleanup of stale and unreliable peers +- Configurable maximum peer limit +- Peer reliability tracking (success/failure counts) +- Atomic file operations for data integrity + +## License + +This SAFE Network Software is licensed under the General Public License (GPL), version 3 ([LICENSE](LICENSE) http://www.gnu.org/licenses/gpl-3.0.en.html). diff --git a/ant-bootstrap/src/cache_store.rs b/ant-bootstrap/src/cache_store.rs new file mode 100644 index 0000000000..c435fbec23 --- /dev/null +++ b/ant-bootstrap/src/cache_store.rs @@ -0,0 +1,459 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use crate::{ + craft_valid_multiaddr, multiaddr_get_peer_id, BootstrapAddr, BootstrapAddresses, + BootstrapCacheConfig, Error, PeersArgs, Result, +}; +use atomic_write_file::AtomicWriteFile; +use libp2p::{multiaddr::Protocol, Multiaddr, PeerId}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{hash_map::Entry, HashMap}, + fs::{self, OpenOptions}, + io::{Read, Write}, + path::PathBuf, + time::{Duration, SystemTime}, +}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CacheData { + pub(crate) peers: std::collections::HashMap, + #[serde(default = "SystemTime::now")] + last_updated: SystemTime, + #[serde(default = "default_version")] + version: u32, +} + +impl CacheData { + pub fn insert(&mut self, peer_id: PeerId, bootstrap_addr: BootstrapAddr) { + match self.peers.entry(peer_id) { + Entry::Occupied(mut occupied_entry) => { + occupied_entry.get_mut().insert_addr(&bootstrap_addr); + } + Entry::Vacant(vacant_entry) => { + vacant_entry.insert(BootstrapAddresses(vec![bootstrap_addr])); + } + } + } + + /// Sync the self cache with another cache. This would just add the 'other' state to self. + pub fn sync(&mut self, other: &CacheData) { + for (peer, other_addresses_state) in other.peers.iter() { + let bootstrap_addresses = self + .peers + .entry(*peer) + .or_insert(other_addresses_state.clone()); + + trace!("Syncing {peer:?} from other with addrs count: {:?}. Our in memory state count: {:?}", other_addresses_state.0.len(), bootstrap_addresses.0.len()); + + bootstrap_addresses.sync(other_addresses_state); + } + + self.last_updated = SystemTime::now(); + } + + /// Perform cleanup on the Peers + /// - Removes all the unreliable addrs for a peer + /// - Removes all the expired addrs for a peer + /// - Removes all peers with empty addrs set + /// - Maintains `max_addr` per peer by removing the addr with the lowest success rate + /// - Maintains `max_peers` in the list by removing the peer with the oldest last_seen + pub fn perform_cleanup(&mut self, cfg: &BootstrapCacheConfig) { + self.peers.values_mut().for_each(|bootstrap_addresses| { + bootstrap_addresses.0.retain(|bootstrap_addr| { + let now = SystemTime::now(); + let has_not_expired = + if let Ok(duration) = now.duration_since(bootstrap_addr.last_seen) { + duration < cfg.addr_expiry_duration + } else { + false + }; + bootstrap_addr.is_reliable() && has_not_expired + }) + }); + + self.peers + .retain(|_, bootstrap_addresses| !bootstrap_addresses.0.is_empty()); + + self.peers.values_mut().for_each(|bootstrap_addresses| { + if bootstrap_addresses.0.len() > cfg.max_addrs_per_peer { + // sort by lowest failure rate first + bootstrap_addresses + .0 + .sort_by_key(|addr| addr.failure_rate() as u64); + bootstrap_addresses.0.truncate(cfg.max_addrs_per_peer); + } + }); + + self.try_remove_oldest_peers(cfg); + } + + /// Remove the oldest peers until we're under the max_peers limit + pub fn try_remove_oldest_peers(&mut self, cfg: &BootstrapCacheConfig) { + if self.peers.len() > cfg.max_peers { + let mut peer_last_seen_map = HashMap::new(); + for (peer, addrs) in self.peers.iter() { + let mut latest_seen = Duration::from_secs(u64::MAX); + for addr in addrs.0.iter() { + if let Ok(elapsed) = addr.last_seen.elapsed() { + trace!("Time elapsed for {addr:?} is {elapsed:?}"); + if elapsed < latest_seen { + trace!("Updating latest_seen to {elapsed:?}"); + latest_seen = elapsed; + } + } + } + trace!("Last seen for {peer:?} is {latest_seen:?}"); + peer_last_seen_map.insert(*peer, latest_seen); + } + + while self.peers.len() > cfg.max_peers { + // find the peer with the largest last_seen + if let Some((&oldest_peer, last_seen)) = peer_last_seen_map + .iter() + .max_by_key(|(_, last_seen)| **last_seen) + { + debug!("Found the oldest peer to remove: {oldest_peer:?} with last_seen of {last_seen:?}"); + self.peers.remove(&oldest_peer); + peer_last_seen_map.remove(&oldest_peer); + } + } + } + } +} + +fn default_version() -> u32 { + 1 +} + +impl Default for CacheData { + fn default() -> Self { + Self { + peers: std::collections::HashMap::new(), + last_updated: SystemTime::now(), + version: default_version(), + } + } +} + +#[derive(Clone, Debug)] +pub struct BootstrapCacheStore { + pub(crate) cache_path: PathBuf, + pub(crate) config: BootstrapCacheConfig, + pub(crate) data: CacheData, +} + +impl BootstrapCacheStore { + pub fn config(&self) -> &BootstrapCacheConfig { + &self.config + } + + /// Create a empty CacheStore with the given configuration + pub fn new(config: BootstrapCacheConfig) -> Result { + info!("Creating new CacheStore with config: {:?}", config); + let cache_path = config.cache_file_path.clone(); + + // Create cache directory if it doesn't exist + if let Some(parent) = cache_path.parent() { + if !parent.exists() { + info!("Attempting to create cache directory at {parent:?}"); + fs::create_dir_all(parent).inspect_err(|err| { + warn!("Failed to create cache directory at {parent:?}: {err}"); + })?; + } + } + + let store = Self { + cache_path, + config, + data: CacheData::default(), + }; + + Ok(store) + } + + /// Create a empty CacheStore from the given peers argument. + /// This also modifies the cfg if provided based on the PeersArgs. + /// And also performs some actions based on the PeersArgs. + pub fn new_from_peers_args( + peers_arg: &PeersArgs, + cfg: Option, + ) -> Result { + let config = if let Some(cfg) = cfg { + cfg + } else { + BootstrapCacheConfig::default_config()? + }; + let mut store = Self::new(config)?; + + // If it is the first node, clear the cache. + if peers_arg.first { + info!("First node in network, writing empty cache to disk"); + store.write()?; + } + + // If local mode is enabled, return empty store (will use mDNS) + if peers_arg.local || cfg!(feature = "local") { + info!("Setting config to not write to cache, as 'local' mode is enabled"); + store.config.disable_cache_writing = true; + } + + Ok(store) + } + + /// Load cache data from disk + /// Make sure to have clean addrs inside the cache as we don't call craft_valid_multiaddr + pub fn load_cache_data(cfg: &BootstrapCacheConfig) -> Result { + // Try to open the file with read permissions + let mut file = OpenOptions::new() + .read(true) + .open(&cfg.cache_file_path) + .inspect_err(|err| warn!("Failed to open cache file: {err}",))?; + + // Read the file contents + let mut contents = String::new(); + file.read_to_string(&mut contents).inspect_err(|err| { + warn!("Failed to read cache file: {err}"); + })?; + + // Parse the cache data + let mut data = serde_json::from_str::(&contents).map_err(|err| { + warn!("Failed to parse cache data: {err}"); + Error::FailedToParseCacheData + })?; + + data.perform_cleanup(cfg); + + Ok(data) + } + + pub fn peer_count(&self) -> usize { + self.data.peers.len() + } + + pub fn get_all_addrs(&self) -> impl Iterator { + self.data + .peers + .values() + .flat_map(|bootstrap_addresses| bootstrap_addresses.0.iter()) + } + + /// Get a list containing single addr per peer. We use the least faulty addr for each peer. + /// This list is sorted by the failure rate of the addr. + pub fn get_sorted_addrs(&self) -> impl Iterator { + let mut addrs = self + .data + .peers + .values() + .flat_map(|bootstrap_addresses| bootstrap_addresses.get_least_faulty()) + .collect::>(); + + addrs.sort_by_key(|addr| addr.failure_rate() as u64); + + addrs.into_iter().map(|addr| &addr.addr) + } + + /// Update the status of an addr in the cache. The peer must be added to the cache first. + pub fn update_addr_status(&mut self, addr: &Multiaddr, success: bool) { + if let Some(peer_id) = multiaddr_get_peer_id(addr) { + debug!("Updating addr status: {addr} (success: {success})"); + if let Some(bootstrap_addresses) = self.data.peers.get_mut(&peer_id) { + bootstrap_addresses.update_addr_status(addr, success); + } else { + debug!("Peer not found in cache to update: {addr}"); + } + } + } + + /// Add a set of addresses to the cache. + pub fn add_addr(&mut self, addr: Multiaddr) { + debug!("Trying to add new addr: {addr}"); + let Some(addr) = craft_valid_multiaddr(&addr, false) else { + return; + }; + let peer_id = match addr.iter().find(|p| matches!(p, Protocol::P2p(_))) { + Some(Protocol::P2p(id)) => id, + _ => return, + }; + + // Check if we already have this peer + if let Some(bootstrap_addrs) = self.data.peers.get_mut(&peer_id) { + if let Some(bootstrap_addr) = bootstrap_addrs.get_addr_mut(&addr) { + debug!("Updating existing peer's last_seen {addr}"); + bootstrap_addr.last_seen = SystemTime::now(); + return; + } else { + let mut bootstrap_addr = BootstrapAddr::new(addr.clone()); + bootstrap_addr.success_count = 1; + bootstrap_addrs.insert_addr(&bootstrap_addr); + } + } else { + let mut bootstrap_addr = BootstrapAddr::new(addr.clone()); + bootstrap_addr.success_count = 1; + self.data + .peers + .insert(peer_id, BootstrapAddresses(vec![bootstrap_addr])); + } + + debug!("Added new peer {addr:?}, performing cleanup of old addrs"); + self.perform_cleanup(); + } + + /// Remove a single address for a peer. + pub fn remove_addr(&mut self, addr: &Multiaddr) { + if let Some(peer_id) = multiaddr_get_peer_id(addr) { + if let Some(bootstrap_addresses) = self.data.peers.get_mut(&peer_id) { + bootstrap_addresses.remove_addr(addr); + } else { + debug!("Peer {peer_id:?} not found in the cache. Not removing addr: {addr:?}") + } + } else { + debug!("Could not obtain PeerId for {addr:?}, not removing addr from cache."); + } + } + + pub fn perform_cleanup(&mut self) { + self.data.perform_cleanup(&self.config); + } + + /// Flush the cache to disk after syncing with the CacheData from the file. + /// Do not perform cleanup when `data` is fetched from the network. The SystemTime might not be accurate. + pub fn sync_and_flush_to_disk(&mut self, with_cleanup: bool) -> Result<()> { + if self.config.disable_cache_writing { + info!("Cache writing is disabled, skipping sync to disk"); + return Ok(()); + } + + info!( + "Flushing cache to disk, with data containing: {} peers", + self.data.peers.len(), + ); + + if let Ok(data_from_file) = Self::load_cache_data(&self.config) { + self.data.sync(&data_from_file); + } else { + warn!("Failed to load cache data from file, overwriting with new data"); + } + + if with_cleanup { + self.data.perform_cleanup(&self.config); + self.data.try_remove_oldest_peers(&self.config); + } + + self.write().inspect_err(|e| { + error!("Failed to save cache to disk: {e}"); + })?; + + // Flush after writing + self.data.peers.clear(); + + Ok(()) + } + + /// Write the cache to disk atomically. This will overwrite the existing cache file, use sync_and_flush_to_disk to + /// sync with the file first. + pub fn write(&self) -> Result<()> { + debug!("Writing cache to disk: {:?}", self.cache_path); + // Create parent directory if it doesn't exist + if let Some(parent) = self.cache_path.parent() { + fs::create_dir_all(parent)?; + } + + let mut file = AtomicWriteFile::options() + .open(&self.cache_path) + .inspect_err(|err| { + error!("Failed to open cache file using AtomicWriteFile: {err}"); + })?; + + let data = serde_json::to_string_pretty(&self.data).inspect_err(|err| { + error!("Failed to serialize cache data: {err}"); + })?; + writeln!(file, "{data}")?; + file.commit().inspect_err(|err| { + error!("Failed to commit atomic write: {err}"); + })?; + + info!("Cache written to disk: {:?}", self.cache_path); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + async fn create_test_store() -> (BootstrapCacheStore, PathBuf) { + let temp_dir = tempdir().unwrap(); + let cache_file = temp_dir.path().join("cache.json"); + + let config = crate::BootstrapCacheConfig::empty().with_cache_path(&cache_file); + + let store = BootstrapCacheStore::new(config).unwrap(); + (store.clone(), store.cache_path.clone()) + } + + #[tokio::test] + async fn test_peer_cleanup() { + let (mut store, _) = create_test_store().await; + let good_addr: Multiaddr = + "/ip4/127.0.0.1/tcp/8080/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE" + .parse() + .unwrap(); + let bad_addr: Multiaddr = + "/ip4/127.0.0.1/tcp/8081/p2p/12D3KooWD2aV1f3qkhggzEFaJ24CEFYkSdZF5RKoMLpU6CwExYV5" + .parse() + .unwrap(); + + // Add peers + store.add_addr(good_addr.clone()); + store.add_addr(bad_addr.clone()); + + // Make one peer reliable and one unreliable + store.update_addr_status(&good_addr, true); + + // Fail the bad peer more times than max_retries + for _ in 0..5 { + store.update_addr_status(&bad_addr, false); + } + + // Clean up unreliable peers + store.perform_cleanup(); + + // Get all peers (not just reliable ones) + let peers = store.get_all_addrs().collect::>(); + assert_eq!(peers.len(), 1); + assert_eq!(peers[0].addr, good_addr); + } + + #[tokio::test] + async fn test_peer_not_removed_if_successful() { + let (mut store, _) = create_test_store().await; + let addr: Multiaddr = + "/ip4/127.0.0.1/tcp/8080/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE" + .parse() + .unwrap(); + + // Add a peer and make it successful + store.add_addr(addr.clone()); + store.update_addr_status(&addr, true); + + // Wait a bit + tokio::time::sleep(Duration::from_millis(100)).await; + + // Run cleanup + store.perform_cleanup(); + + // Verify peer is still there + let peers = store.get_all_addrs().collect::>(); + assert_eq!(peers.len(), 1); + assert_eq!(peers[0].addr, addr); + } +} diff --git a/ant-bootstrap/src/config.rs b/ant-bootstrap/src/config.rs new file mode 100644 index 0000000000..52d85b7dee --- /dev/null +++ b/ant-bootstrap/src/config.rs @@ -0,0 +1,125 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use crate::error::{Error, Result}; +use ant_protocol::version::{get_key_version_str, get_truncate_version_str}; +use std::{ + path::{Path, PathBuf}, + time::Duration, +}; + +/// The duration since last)seen before removing the address of a Peer. +const ADDR_EXPIRY_DURATION: Duration = Duration::from_secs(24 * 60 * 60); // 24 hours + +/// Maximum peers to store +const MAX_PEERS: usize = 1500; + +/// Maximum number of addresses to store for a Peer +const MAX_ADDRS_PER_PEER: usize = 6; + +// Min time until we save the bootstrap cache to disk. 5 mins +const MIN_BOOTSTRAP_CACHE_SAVE_INTERVAL: Duration = Duration::from_secs(5 * 60); + +// Max time until we save the bootstrap cache to disk. 24 hours +const MAX_BOOTSTRAP_CACHE_SAVE_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); + +/// Configuration for the bootstrap cache +#[derive(Clone, Debug)] +pub struct BootstrapCacheConfig { + /// The duration since last)seen before removing the address of a Peer. + pub addr_expiry_duration: Duration, + /// Maximum number of peers to keep in the cache + pub max_peers: usize, + /// Maximum number of addresses stored per peer. + pub max_addrs_per_peer: usize, + /// Path to the bootstrap cache file + pub cache_file_path: PathBuf, + /// Flag to disable writing to the cache file + pub disable_cache_writing: bool, + /// The min time duration until we save the bootstrap cache to disk. + pub min_cache_save_duration: Duration, + /// The max time duration until we save the bootstrap cache to disk. + pub max_cache_save_duration: Duration, + /// The cache save scaling factor. We start with the min_cache_save_duration and scale it up to the max_cache_save_duration. + pub cache_save_scaling_factor: u64, +} + +impl BootstrapCacheConfig { + /// Creates a new BootstrapConfig with default settings + pub fn default_config() -> Result { + Ok(Self { + addr_expiry_duration: ADDR_EXPIRY_DURATION, + max_peers: MAX_PEERS, + max_addrs_per_peer: MAX_ADDRS_PER_PEER, + cache_file_path: default_cache_path()?, + disable_cache_writing: false, + min_cache_save_duration: MIN_BOOTSTRAP_CACHE_SAVE_INTERVAL, + max_cache_save_duration: MAX_BOOTSTRAP_CACHE_SAVE_INTERVAL, + cache_save_scaling_factor: 2, + }) + } + + /// Creates a new BootstrapConfig with empty settings + pub fn empty() -> Self { + Self { + addr_expiry_duration: ADDR_EXPIRY_DURATION, + max_peers: MAX_PEERS, + max_addrs_per_peer: MAX_ADDRS_PER_PEER, + cache_file_path: PathBuf::new(), + disable_cache_writing: false, + min_cache_save_duration: MIN_BOOTSTRAP_CACHE_SAVE_INTERVAL, + max_cache_save_duration: MAX_BOOTSTRAP_CACHE_SAVE_INTERVAL, + cache_save_scaling_factor: 2, + } + } + + /// Set a new addr expiry duration + pub fn with_addr_expiry_duration(mut self, duration: Duration) -> Self { + self.addr_expiry_duration = duration; + self + } + + /// Update the config with a custom cache file path + pub fn with_cache_path>(mut self, path: P) -> Self { + self.cache_file_path = path.as_ref().to_path_buf(); + self + } + + /// Sets the maximum number of peers + pub fn with_max_peers(mut self, max_peers: usize) -> Self { + self.max_peers = max_peers; + self + } + + /// Sets the maximum number of addresses for a single peer. + pub fn with_addrs_per_peer(mut self, max_addrs: usize) -> Self { + self.max_addrs_per_peer = max_addrs; + self + } + + /// Sets the flag to disable writing to the cache file + pub fn with_disable_cache_writing(mut self, disable: bool) -> Self { + self.disable_cache_writing = disable; + self + } +} + +/// Returns the default path for the bootstrap cache file +fn default_cache_path() -> Result { + let dir = dirs_next::data_dir() + .ok_or_else(|| Error::CouldNotObtainDataDir)? + .join("autonomi") + .join("bootstrap_cache"); + + std::fs::create_dir_all(&dir)?; + + let network_id = format!("{}_{}", get_key_version_str(), get_truncate_version_str()); + let path = dir.join(format!("bootstrap_cache_{}.json", network_id)); + + Ok(path) +} diff --git a/ant-bootstrap/src/contacts.rs b/ant-bootstrap/src/contacts.rs new file mode 100644 index 0000000000..83262fbc1a --- /dev/null +++ b/ant-bootstrap/src/contacts.rs @@ -0,0 +1,439 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use crate::{cache_store::CacheData, craft_valid_multiaddr_from_str, BootstrapAddr, Error, Result}; +use futures::stream::{self, StreamExt}; +use libp2p::Multiaddr; +use reqwest::Client; +use std::time::Duration; +use url::Url; + +/// The client fetch timeout +#[cfg(not(target_arch = "wasm32"))] +const FETCH_TIMEOUT_SECS: u64 = 30; +/// Maximum number of endpoints to fetch at a time +const MAX_CONCURRENT_FETCHES: usize = 3; +/// The max number of retries for a endpoint on failure. +const MAX_RETRIES_ON_FETCH_FAILURE: usize = 3; + +/// Discovers initial peers from a list of endpoints +pub struct ContactsFetcher { + /// The list of endpoints + endpoints: Vec, + /// Reqwest Client + request_client: Client, + /// Ignore PeerId in the multiaddr if not present. This is only useful for fetching nat detection contacts + ignore_peer_id: bool, +} + +impl ContactsFetcher { + /// Create a new struct with the default endpoint + pub fn new() -> Result { + Self::with_endpoints(vec![]) + } + + /// Create a new struct with the provided endpoints + pub fn with_endpoints(endpoints: Vec) -> Result { + #[cfg(not(target_arch = "wasm32"))] + let request_client = Client::builder() + .timeout(Duration::from_secs(FETCH_TIMEOUT_SECS)) + .build()?; + // Wasm does not have the timeout method yet. + #[cfg(target_arch = "wasm32")] + let request_client = Client::builder().build()?; + + Ok(Self { + endpoints, + request_client, + ignore_peer_id: false, + }) + } + + /// Create a new struct with the mainnet endpoints + pub fn with_mainnet_endpoints() -> Result { + let mut fetcher = Self::new()?; + let mainnet_contact = vec![ + "https://sn-testnet.s3.eu-west-2.amazonaws.com/bootstrap_cache.json" + .parse() + .expect("Failed to parse URL"), + "https://sn-testnet.s3.eu-west-2.amazonaws.com/network-contacts" + .parse() + .expect("Failed to parse URL"), + ]; + fetcher.endpoints = mainnet_contact; + Ok(fetcher) + } + + pub fn insert_endpoint(&mut self, endpoint: Url) { + self.endpoints.push(endpoint); + } + + pub fn ignore_peer_id(&mut self, ignore_peer_id: bool) { + self.ignore_peer_id = ignore_peer_id; + } + + /// Fetch the list of bootstrap addresses from all configured endpoints + pub async fn fetch_bootstrap_addresses(&self) -> Result> { + Ok(self + .fetch_addrs() + .await? + .into_iter() + .map(BootstrapAddr::new) + .collect()) + } + + /// Fetch the list of multiaddrs from all configured endpoints + pub async fn fetch_addrs(&self) -> Result> { + info!( + "Starting peer fetcher from {} endpoints: {:?}", + self.endpoints.len(), + self.endpoints + ); + let mut bootstrap_addresses = Vec::new(); + let mut last_error = None; + + let mut fetches = stream::iter(self.endpoints.clone()) + .map(|endpoint| async move { + info!( + "Attempting to fetch bootstrap addresses from endpoint: {}", + endpoint + ); + ( + Self::fetch_from_endpoint( + self.request_client.clone(), + &endpoint, + self.ignore_peer_id, + ) + .await, + endpoint, + ) + }) + .buffer_unordered(MAX_CONCURRENT_FETCHES); + + while let Some((result, endpoint)) = fetches.next().await { + match result { + Ok(mut endpoing_bootstrap_addresses) => { + info!( + "Successfully fetched {} bootstrap addrs from {}. First few addrs: {:?}", + endpoing_bootstrap_addresses.len(), + endpoint, + endpoing_bootstrap_addresses + .iter() + .take(3) + .collect::>() + ); + bootstrap_addresses.append(&mut endpoing_bootstrap_addresses); + } + Err(e) => { + warn!("Failed to fetch bootstrap addrs from {}: {}", endpoint, e); + last_error = Some(e); + } + } + } + + if bootstrap_addresses.is_empty() { + last_error.map_or_else( + || { + warn!("No bootstrap addrs found from any endpoint and no errors reported"); + Err(Error::NoBootstrapAddressesFound( + "No valid peers found from any endpoint".to_string(), + )) + }, + |e| { + warn!( + "No bootstrap addrs found from any endpoint. Last error: {}", + e + ); + Err(Error::NoBootstrapAddressesFound(format!( + "No valid bootstrap addrs found from any endpoint: {e}", + ))) + }, + ) + } else { + info!( + "Successfully discovered {} total addresses. First few: {:?}", + bootstrap_addresses.len(), + bootstrap_addresses.iter().take(3).collect::>() + ); + Ok(bootstrap_addresses) + } + } + + /// Fetch the list of multiaddrs from a single endpoint + async fn fetch_from_endpoint( + request_client: Client, + endpoint: &Url, + ignore_peer_id: bool, + ) -> Result> { + info!("Fetching peers from endpoint: {endpoint}"); + let mut retries = 0; + + let bootstrap_addresses = loop { + let response = request_client.get(endpoint.clone()).send().await; + + match response { + Ok(response) => { + if response.status().is_success() { + let text = response.text().await?; + + match Self::try_parse_response(&text, ignore_peer_id) { + Ok(addrs) => break addrs, + Err(err) => { + warn!("Failed to parse response with err: {err:?}"); + retries += 1; + if retries >= MAX_RETRIES_ON_FETCH_FAILURE { + return Err(Error::FailedToObtainAddrsFromUrl( + endpoint.to_string(), + MAX_RETRIES_ON_FETCH_FAILURE, + )); + } + } + } + } else { + retries += 1; + if retries >= MAX_RETRIES_ON_FETCH_FAILURE { + return Err(Error::FailedToObtainAddrsFromUrl( + endpoint.to_string(), + MAX_RETRIES_ON_FETCH_FAILURE, + )); + } + } + } + Err(err) => { + error!("Failed to get bootstrap addrs from URL {endpoint}: {err:?}"); + retries += 1; + if retries >= MAX_RETRIES_ON_FETCH_FAILURE { + return Err(Error::FailedToObtainAddrsFromUrl( + endpoint.to_string(), + MAX_RETRIES_ON_FETCH_FAILURE, + )); + } + } + } + trace!( + "Failed to get bootstrap addrs from URL, retrying {retries}/{MAX_RETRIES_ON_FETCH_FAILURE}" + ); + + #[cfg(not(target_arch = "wasm32"))] + tokio::time::sleep(Duration::from_secs(1)).await; + #[cfg(target_arch = "wasm32")] + wasmtimer::tokio::sleep(Duration::from_secs(1)).await; + }; + + Ok(bootstrap_addresses) + } + + /// Try to parse a response from a endpoint + fn try_parse_response(response: &str, ignore_peer_id: bool) -> Result> { + match serde_json::from_str::(response) { + Ok(json_endpoints) => { + info!( + "Successfully parsed JSON response with {} peers", + json_endpoints.peers.len() + ); + let bootstrap_addresses = json_endpoints + .peers + .into_iter() + .filter_map(|(_, addresses)| { + addresses.get_least_faulty().map(|addr| addr.addr.clone()) + }) + .collect::>(); + + if bootstrap_addresses.is_empty() { + warn!("No valid peers found in JSON response"); + Err(Error::NoBootstrapAddressesFound( + "No valid peers found in JSON response".to_string(), + )) + } else { + info!( + "Successfully parsed {} valid peers from JSON", + bootstrap_addresses.len() + ); + Ok(bootstrap_addresses) + } + } + Err(e) => { + info!("Attempting to parse response as plain text"); + // Try parsing as plain text with one multiaddr per line + // example of contacts file exists in resources/network-contacts-examples + let bootstrap_addresses = response + .split('\n') + .filter_map(|str| craft_valid_multiaddr_from_str(str, ignore_peer_id)) + .collect::>(); + + if bootstrap_addresses.is_empty() { + warn!( + "No valid bootstrap addrs found in plain text response. Previous Json error: {e:?}" + ); + Err(Error::NoBootstrapAddressesFound( + "No valid bootstrap addrs found in plain text response".to_string(), + )) + } else { + info!( + "Successfully parsed {} valid bootstrap addrs from plain text", + bootstrap_addresses.len() + ); + Ok(bootstrap_addresses) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use libp2p::Multiaddr; + use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, + }; + + #[tokio::test] + async fn test_fetch_addrs() { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/")) + .respond_with( + ResponseTemplate::new(200) + .set_body_string("/ip4/127.0.0.1/tcp/8080/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE\n/ip4/127.0.0.2/tcp/8080/p2p/12D3KooWD2aV1f3qkhggzEFaJ24CEFYkSdZF5RKoMLpU6CwExYV5"), + ) + .mount(&mock_server) + .await; + + let mut fetcher = ContactsFetcher::new().unwrap(); + fetcher.endpoints = vec![mock_server.uri().parse().unwrap()]; + + let addrs = fetcher.fetch_bootstrap_addresses().await.unwrap(); + assert_eq!(addrs.len(), 2); + + let addr1: Multiaddr = + "/ip4/127.0.0.1/tcp/8080/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE" + .parse() + .unwrap(); + let addr2: Multiaddr = + "/ip4/127.0.0.2/tcp/8080/p2p/12D3KooWD2aV1f3qkhggzEFaJ24CEFYkSdZF5RKoMLpU6CwExYV5" + .parse() + .unwrap(); + assert!(addrs.iter().any(|p| p.addr == addr1)); + assert!(addrs.iter().any(|p| p.addr == addr2)); + } + + #[tokio::test] + async fn test_endpoint_failover() { + let mock_server1 = MockServer::start().await; + let mock_server2 = MockServer::start().await; + + // First endpoint fails + Mock::given(method("GET")) + .and(path("/")) + .respond_with(ResponseTemplate::new(500)) + .mount(&mock_server1) + .await; + + // Second endpoint succeeds + Mock::given(method("GET")) + .and(path("/")) + .respond_with(ResponseTemplate::new(200).set_body_string( + "/ip4/127.0.0.1/tcp/8080/p2p/12D3KooWD2aV1f3qkhggzEFaJ24CEFYkSdZF5RKoMLpU6CwExYV5", + )) + .mount(&mock_server2) + .await; + + let mut fetcher = ContactsFetcher::new().unwrap(); + fetcher.endpoints = vec![ + mock_server1.uri().parse().unwrap(), + mock_server2.uri().parse().unwrap(), + ]; + + let addrs = fetcher.fetch_bootstrap_addresses().await.unwrap(); + assert_eq!(addrs.len(), 1); + + let addr: Multiaddr = + "/ip4/127.0.0.1/tcp/8080/p2p/12D3KooWD2aV1f3qkhggzEFaJ24CEFYkSdZF5RKoMLpU6CwExYV5" + .parse() + .unwrap(); + assert_eq!(addrs[0].addr, addr); + } + + #[tokio::test] + async fn test_invalid_multiaddr() { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/")) + .respond_with( + ResponseTemplate::new(200).set_body_string( + "/ip4/127.0.0.1/tcp/8080\n/ip4/127.0.0.2/tcp/8080/p2p/12D3KooWD2aV1f3qkhggzEFaJ24CEFYkSdZF5RKoMLpU6CwExYV5", + ), + ) + .mount(&mock_server) + .await; + + let mut fetcher = ContactsFetcher::new().unwrap(); + fetcher.endpoints = vec![mock_server.uri().parse().unwrap()]; + + let addrs = fetcher.fetch_bootstrap_addresses().await.unwrap(); + let valid_addr: Multiaddr = + "/ip4/127.0.0.2/tcp/8080/p2p/12D3KooWD2aV1f3qkhggzEFaJ24CEFYkSdZF5RKoMLpU6CwExYV5" + .parse() + .unwrap(); + assert_eq!(addrs[0].addr, valid_addr); + } + + #[tokio::test] + async fn test_empty_response() { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/")) + .respond_with(ResponseTemplate::new(200).set_body_string("")) + .mount(&mock_server) + .await; + + let mut fetcher = ContactsFetcher::new().unwrap(); + fetcher.endpoints = vec![mock_server.uri().parse().unwrap()]; + + let result = fetcher.fetch_bootstrap_addresses().await; + + assert!(matches!(result, Err(Error::NoBootstrapAddressesFound(_)))); + } + + #[tokio::test] + async fn test_whitespace_and_empty_lines() { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/")) + .respond_with( + ResponseTemplate::new(200).set_body_string("\n \n/ip4/127.0.0.1/tcp/8080/p2p/12D3KooWD2aV1f3qkhggzEFaJ24CEFYkSdZF5RKoMLpU6CwExYV5\n \n"), + ) + .mount(&mock_server) + .await; + + let mut fetcher = ContactsFetcher::new().unwrap(); + fetcher.endpoints = vec![mock_server.uri().parse().unwrap()]; + + let addrs = fetcher.fetch_bootstrap_addresses().await.unwrap(); + assert_eq!(addrs.len(), 1); + + let addr: Multiaddr = + "/ip4/127.0.0.1/tcp/8080/p2p/12D3KooWD2aV1f3qkhggzEFaJ24CEFYkSdZF5RKoMLpU6CwExYV5" + .parse() + .unwrap(); + assert_eq!(addrs[0].addr, addr); + } + + #[tokio::test] + async fn test_custom_endpoints() { + let endpoints = vec!["http://example.com".parse().unwrap()]; + let fetcher = ContactsFetcher::with_endpoints(endpoints.clone()).unwrap(); + assert_eq!(fetcher.endpoints, endpoints); + } +} diff --git a/ant-bootstrap/src/error.rs b/ant-bootstrap/src/error.rs new file mode 100644 index 0000000000..77002702e5 --- /dev/null +++ b/ant-bootstrap/src/error.rs @@ -0,0 +1,33 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Failed to obtain any bootstrap peers")] + NoBootstrapPeersFound, + #[error("Failed to parse cache data")] + FailedToParseCacheData, + #[error("Could not obtain data directory")] + CouldNotObtainDataDir, + #[error("Could not obtain bootstrap addresses from {0} after {1} retries")] + FailedToObtainAddrsFromUrl(String, usize), + #[error("No Bootstrap Addresses found: {0}")] + NoBootstrapAddressesFound(String), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + #[error("Lock error")] + LockError, +} + +pub type Result = std::result::Result; diff --git a/ant-bootstrap/src/initial_peers.rs b/ant-bootstrap/src/initial_peers.rs new file mode 100644 index 0000000000..07d0cd3b24 --- /dev/null +++ b/ant-bootstrap/src/initial_peers.rs @@ -0,0 +1,192 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use crate::{ + craft_valid_multiaddr, craft_valid_multiaddr_from_str, + error::{Error, Result}, + BootstrapAddr, BootstrapCacheConfig, BootstrapCacheStore, ContactsFetcher, +}; +use clap::Args; +use libp2p::Multiaddr; +use url::Url; + +/// The name of the environment variable that can be used to pass peers to the node. +pub const ANT_PEERS_ENV: &str = "ANT_PEERS"; + +/// Command line arguments for peer configuration +#[derive(Args, Debug, Clone, Default)] +pub struct PeersArgs { + /// Set to indicate this is the first node in a new network + /// + /// If this argument is used, any others will be ignored because they do not apply to the first + /// node. + #[clap(long, default_value = "false")] + pub first: bool, + /// Addr(s) to use for bootstrap, in a 'multiaddr' format containing the peer ID. + /// + /// A multiaddr looks like + /// '/ip4/1.2.3.4/tcp/1200/tcp/p2p/12D3KooWRi6wF7yxWLuPSNskXc6kQ5cJ6eaymeMbCRdTnMesPgFx' where + /// `1.2.3.4` is the IP, `1200` is the port and the (optional) last part is the peer ID. + /// + /// This argument can be provided multiple times to connect to multiple peers. + /// + /// Alternatively, the `ANT_PEERS` environment variable can provide a comma-separated peer + /// list. + #[clap( + long = "peer", + value_name = "multiaddr", + value_delimiter = ',', + conflicts_with = "first", + value_parser = parse_multiaddr_str + )] + pub addrs: Vec, + /// Specify the URL to fetch the network contacts from. + /// + /// The URL can point to a text file containing Multiaddresses separated by newline character, or + /// a bootstrap cache JSON file. + #[clap(long, conflicts_with = "first")] + pub network_contacts_url: Option, + /// Set to indicate this is a local network. You could also set the `local` feature flag to set this to true. + /// + /// This would use mDNS for peer discovery. + #[clap(long, conflicts_with = "network_contacts_url", default_value = "false")] + pub local: bool, + /// Set to indicate this is a testnet. + /// + /// This disables fetching peers from the mainnet network contacts. + #[clap(name = "testnet", long, conflicts_with = "network_contacts_url")] + pub disable_mainnet_contacts: bool, + + /// Set to not load the bootstrap addresses from the local cache. + #[clap(long, default_value = "false")] + pub ignore_cache: bool, +} +impl PeersArgs { + /// Get bootstrap peers + /// Order of precedence: + /// 1. Addresses from arguments + /// 2. Addresses from environment variable SAFE_PEERS + /// 3. Addresses from cache + /// 4. Addresses from network contacts URL + pub async fn get_addrs(&self, config: Option) -> Result> { + Ok(self + .get_bootstrap_addr(config) + .await? + .into_iter() + .map(|addr| addr.addr) + .collect()) + } + + /// Get bootstrap peers + /// Order of precedence: + /// 1. Addresses from arguments + /// 2. Addresses from environment variable SAFE_PEERS + /// 3. Addresses from cache + /// 4. Addresses from network contacts URL + pub async fn get_bootstrap_addr( + &self, + config: Option, + ) -> Result> { + // If this is the first node, return an empty list + if self.first { + info!("First node in network, no initial bootstrap peers"); + return Ok(vec![]); + } + + // If local mode is enabled, return empty store (will use mDNS) + if self.local || cfg!(feature = "local") { + info!("Local mode enabled, using only local discovery."); + return Ok(vec![]); + } + + let mut bootstrap_addresses = vec![]; + + // Add addrs from arguments if present + for addr in &self.addrs { + if let Some(addr) = craft_valid_multiaddr(addr, false) { + info!("Adding addr from arguments: {addr}"); + bootstrap_addresses.push(BootstrapAddr::new(addr)); + } else { + warn!("Invalid multiaddress format from arguments: {addr}"); + } + } + + // Read from ANT_PEERS environment variable if present + if let Ok(addrs) = std::env::var(ANT_PEERS_ENV) { + for addr_str in addrs.split(',') { + if let Some(addr) = craft_valid_multiaddr_from_str(addr_str, false) { + info!("Adding addr from environment variable: {addr}"); + bootstrap_addresses.push(BootstrapAddr::new(addr)); + } else { + warn!("Invalid multiaddress format from environment variable: {addr_str}"); + } + } + } + + // If we have a network contacts URL, fetch addrs from there. + if let Some(url) = self.network_contacts_url.clone() { + info!("Fetching bootstrap address from network contacts URL: {url}",); + let contacts_fetcher = ContactsFetcher::with_endpoints(vec![url])?; + let addrs = contacts_fetcher.fetch_bootstrap_addresses().await?; + bootstrap_addresses.extend(addrs); + } + + // Return here if we fetched peers from the args + if !bootstrap_addresses.is_empty() { + bootstrap_addresses.sort_by_key(|addr| addr.failure_rate() as u64); + return Ok(bootstrap_addresses); + } + + // load from cache if present + if !self.ignore_cache { + let cfg = if let Some(config) = config { + Some(config) + } else { + BootstrapCacheConfig::default_config().ok() + }; + if let Some(cfg) = cfg { + info!("Loading bootstrap addresses from cache"); + if let Ok(data) = BootstrapCacheStore::load_cache_data(&cfg) { + bootstrap_addresses = data + .peers + .into_iter() + .filter_map(|(_, addrs)| { + addrs + .0 + .into_iter() + .min_by_key(|addr| addr.failure_rate() as u64) + }) + .collect(); + } + } + } + + if !bootstrap_addresses.is_empty() { + bootstrap_addresses.sort_by_key(|addr| addr.failure_rate() as u64); + return Ok(bootstrap_addresses); + } + + if !self.disable_mainnet_contacts { + let contacts_fetcher = ContactsFetcher::with_mainnet_endpoints()?; + let addrs = contacts_fetcher.fetch_bootstrap_addresses().await?; + bootstrap_addresses = addrs; + } + + if !bootstrap_addresses.is_empty() { + bootstrap_addresses.sort_by_key(|addr| addr.failure_rate() as u64); + Ok(bootstrap_addresses) + } else { + error!("No initial bootstrap peers found through any means"); + Err(Error::NoBootstrapPeersFound) + } + } +} + +pub fn parse_multiaddr_str(addr: &str) -> std::result::Result { + addr.parse::() +} diff --git a/ant-bootstrap/src/lib.rs b/ant-bootstrap/src/lib.rs new file mode 100644 index 0000000000..e7cfa21d8b --- /dev/null +++ b/ant-bootstrap/src/lib.rs @@ -0,0 +1,254 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +//! Bootstrap Cache for the Autonomous Network +//! +//! This crate provides a decentralized peer discovery and caching system for the Autonomi Network. +//! It implements a robust peer management system with the following features: +//! +//! - Decentralized Design: No dedicated bootstrap nodes required +//! - Cross-Platform Support: Works on Linux, macOS, and Windows +//! - Shared Cache: System-wide cache file accessible by both nodes and clients +//! - Concurrent Access: File locking for safe multi-process access +//! - Atomic Operations: Safe cache updates using atomic file operations +//! - Initial Peer Discovery: Fallback web endpoints for new/stale cache scenarios + +#[macro_use] +extern crate tracing; + +mod cache_store; +pub mod config; +pub mod contacts; +pub mod error; +mod initial_peers; + +use libp2p::{multiaddr::Protocol, Multiaddr, PeerId}; +use serde::{Deserialize, Serialize}; +use std::time::SystemTime; +use thiserror::Error; + +pub use cache_store::BootstrapCacheStore; +pub use config::BootstrapCacheConfig; +pub use contacts::ContactsFetcher; +pub use error::{Error, Result}; +pub use initial_peers::{PeersArgs, ANT_PEERS_ENV}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +/// Set of addresses for a particular PeerId +pub struct BootstrapAddresses(pub Vec); + +impl BootstrapAddresses { + pub fn insert_addr(&mut self, addr: &BootstrapAddr) { + if let Some(bootstrap_addr) = self.get_addr_mut(&addr.addr) { + bootstrap_addr.sync(addr); + } else { + self.0.push(addr.clone()); + } + } + + pub fn get_addr(&self, addr: &Multiaddr) -> Option<&BootstrapAddr> { + self.0 + .iter() + .find(|bootstrap_addr| &bootstrap_addr.addr == addr) + } + + pub fn get_addr_mut(&mut self, addr: &Multiaddr) -> Option<&mut BootstrapAddr> { + self.0 + .iter_mut() + .find(|bootstrap_addr| &bootstrap_addr.addr == addr) + } + + pub fn get_least_faulty(&self) -> Option<&BootstrapAddr> { + self.0.iter().min_by_key(|addr| addr.failure_rate() as u64) + } + + pub fn remove_addr(&mut self, addr: &Multiaddr) { + if let Some(idx) = self + .0 + .iter() + .position(|bootstrap_addr| &bootstrap_addr.addr == addr) + { + let bootstrap_addr = self.0.remove(idx); + debug!("Removed {bootstrap_addr:?}"); + } + } + + pub fn sync(&mut self, other: &Self) { + for other_addr in other.0.iter() { + if let Some(bootstrap_addr) = self.get_addr_mut(&other_addr.addr) { + bootstrap_addr.sync(other_addr); + } else { + trace!( + "Addr {:?} from other not found in self, inserting it.", + other_addr.addr + ); + self.insert_addr(other_addr); + } + } + } + + pub fn update_addr_status(&mut self, addr: &Multiaddr, success: bool) { + if let Some(bootstrap_addr) = self.get_addr_mut(addr) { + bootstrap_addr.update_status(success); + } else { + debug!("Addr not found in cache to update, skipping: {addr:?}") + } + } +} + +/// A addr that can be used for bootstrapping into the network +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BootstrapAddr { + /// The multiaddress of the peer + pub addr: Multiaddr, + /// The number of successful connections to this address + pub success_count: u32, + /// The number of failed connection attempts to this address + pub failure_count: u32, + /// The last time this address was successfully contacted + pub last_seen: SystemTime, +} + +impl BootstrapAddr { + pub fn new(addr: Multiaddr) -> Self { + Self { + addr, + success_count: 0, + failure_count: 0, + last_seen: SystemTime::now(), + } + } + + pub fn peer_id(&self) -> Option { + multiaddr_get_peer_id(&self.addr) + } + + pub fn update_status(&mut self, success: bool) { + if success { + if let Some(new_value) = self.success_count.checked_add(1) { + self.success_count = new_value; + } else { + self.success_count = 1; + self.failure_count = 0; + } + } + self.last_seen = SystemTime::now(); + if !success { + if let Some(new_value) = self.failure_count.checked_add(1) { + self.failure_count = new_value; + } else { + self.failure_count = 1; + self.success_count = 0; + } + } + } + + // An addr is considered reliable if it has more successes than failures + pub fn is_reliable(&self) -> bool { + self.success_count >= self.failure_count + } + + /// Add the values from other into self. + pub fn sync(&mut self, other: &Self) { + trace!("Syncing our state {self:?} with and other: {other:?}."); + if self.last_seen == other.last_seen { + return; + } + + self.success_count = self.success_count.saturating_add(other.success_count); + self.failure_count = self.failure_count.saturating_add(other.failure_count); + + // if at max value, reset to 0 + if self.success_count == u32::MAX { + self.success_count = 1; + self.failure_count = 0; + } else if self.failure_count == u32::MAX { + self.failure_count = 1; + self.success_count = 0; + } + self.last_seen = std::cmp::max(self.last_seen, other.last_seen); + trace!("Successfully synced BootstrapAddr: {self:?}"); + } + + fn failure_rate(&self) -> f64 { + if self.success_count + self.failure_count == 0 { + 0.0 + } else { + self.failure_count as f64 / (self.success_count + self.failure_count) as f64 + } + } +} + +/// Craft a proper address to avoid any ill formed addresses +/// +/// ignore_peer_id is only used for nat-detection contact list +pub fn craft_valid_multiaddr(addr: &Multiaddr, ignore_peer_id: bool) -> Option { + let peer_id = addr + .iter() + .find(|protocol| matches!(protocol, Protocol::P2p(_))); + + let mut output_address = Multiaddr::empty(); + + let ip = addr + .iter() + .find(|protocol| matches!(protocol, Protocol::Ip4(_)))?; + output_address.push(ip); + + let udp = addr + .iter() + .find(|protocol| matches!(protocol, Protocol::Udp(_))); + let tcp = addr + .iter() + .find(|protocol| matches!(protocol, Protocol::Tcp(_))); + + // UDP or TCP + if let Some(udp) = udp { + output_address.push(udp); + if let Some(quic) = addr + .iter() + .find(|protocol| matches!(protocol, Protocol::QuicV1)) + { + output_address.push(quic); + } + } else if let Some(tcp) = tcp { + output_address.push(tcp); + + if let Some(ws) = addr + .iter() + .find(|protocol| matches!(protocol, Protocol::Ws(_))) + { + output_address.push(ws); + } + } else { + return None; + } + + if let Some(peer_id) = peer_id { + output_address.push(peer_id); + } else if !ignore_peer_id { + return None; + } + + Some(output_address) +} + +/// ignore_peer_id is only used for nat-detection contact list +pub fn craft_valid_multiaddr_from_str(addr_str: &str, ignore_peer_id: bool) -> Option { + let Ok(addr) = addr_str.parse::() else { + warn!("Failed to parse multiaddr from str {addr_str}"); + return None; + }; + craft_valid_multiaddr(&addr, ignore_peer_id) +} + +pub fn multiaddr_get_peer_id(addr: &Multiaddr) -> Option { + match addr.iter().find(|p| matches!(p, Protocol::P2p(_))) { + Some(Protocol::P2p(id)) => Some(id), + _ => None, + } +} diff --git a/ant-bootstrap/tests/address_format_tests.rs b/ant-bootstrap/tests/address_format_tests.rs new file mode 100644 index 0000000000..55d9246b8b --- /dev/null +++ b/ant-bootstrap/tests/address_format_tests.rs @@ -0,0 +1,108 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use ant_bootstrap::{BootstrapCacheConfig, PeersArgs}; +use ant_logging::LogBuilder; +use libp2p::Multiaddr; +use tempfile::TempDir; +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +// Setup function to create a new temp directory and config for each test +async fn setup() -> (TempDir, BootstrapCacheConfig) { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("cache.json"); + + let config = BootstrapCacheConfig::empty() + .with_cache_path(&cache_path) + .with_max_peers(50); + + (temp_dir, config) +} + +#[tokio::test] +async fn test_multiaddr_format_parsing() -> Result<(), Box> { + let _guard = LogBuilder::init_single_threaded_tokio_test("address_format_tests", false); + + // Test various multiaddr formats + let addrs = vec![ + // quic + "/ip4/127.0.0.1/udp/8080/quic-v1/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE", + // ws + "/ip4/127.0.0.1/tcp/8080/ws/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE", + ]; + + for addr_str in addrs { + let (_temp_dir, _config) = setup().await; // Fresh config for each test case + let addr = addr_str.parse::()?; + let args = PeersArgs { + first: false, + addrs: vec![addr.clone()], + network_contacts_url: None, + local: false, + disable_mainnet_contacts: false, + ignore_cache: false, + }; + + let bootstrap_addresses = args.get_bootstrap_addr(None).await?; + assert_eq!(bootstrap_addresses.len(), 1, "Should have one peer"); + assert_eq!( + bootstrap_addresses[0].addr, addr, + "Address format should match" + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_network_contacts_format() -> Result<(), Box> { + let _guard = LogBuilder::init_single_threaded_tokio_test("address_format_tests", false); + + let (_temp_dir, _config) = setup().await; + + // Create a mock server with network contacts format + let mock_server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/peers")) + .respond_with(ResponseTemplate::new(200).set_body_string( + "/ip4/127.0.0.1/udp/8080/quic-v1/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE\n\ + /ip4/127.0.0.2/udp/8081/quic-v1/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERF" + )) + .mount(&mock_server) + .await; + + let args = PeersArgs { + first: false, + addrs: vec![], + network_contacts_url: Some(format!("{}/peers", mock_server.uri()).parse()?), + local: false, + disable_mainnet_contacts: false, + ignore_cache: false, + }; + + let addrs = args.get_bootstrap_addr(None).await?; + assert_eq!( + addrs.len(), + 2, + "Should have two peers from network contacts" + ); + + // Verify address formats + for addr in addrs { + let addr_str = addr.addr.to_string(); + assert!(addr_str.contains("/ip4/"), "Should have IPv4 address"); + assert!(addr_str.contains("/udp/"), "Should have UDP port"); + assert!(addr_str.contains("/quic-v1/"), "Should have QUIC protocol"); + assert!(addr_str.contains("/p2p/"), "Should have peer ID"); + } + + Ok(()) +} diff --git a/ant-bootstrap/tests/cache_tests.rs b/ant-bootstrap/tests/cache_tests.rs new file mode 100644 index 0000000000..4dd9b6edf8 --- /dev/null +++ b/ant-bootstrap/tests/cache_tests.rs @@ -0,0 +1,125 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use ant_bootstrap::{BootstrapCacheConfig, BootstrapCacheStore}; +use ant_logging::LogBuilder; +use libp2p::Multiaddr; +use std::time::Duration; +use tempfile::TempDir; +use tokio::time::sleep; + +#[tokio::test] +async fn test_cache_store_operations() -> Result<(), Box> { + let _guard = LogBuilder::init_single_threaded_tokio_test("cache_tests", false); + + let temp_dir = TempDir::new()?; + let cache_path = temp_dir.path().join("cache.json"); + + // Create cache store with config + let config = BootstrapCacheConfig::empty().with_cache_path(&cache_path); + + let mut cache_store = BootstrapCacheStore::new(config)?; + + // Test adding and retrieving peers + let addr: Multiaddr = + "/ip4/127.0.0.1/udp/8080/quic-v1/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE" + .parse()?; + cache_store.add_addr(addr.clone()); + cache_store.update_addr_status(&addr, true); + + let addrs = cache_store.get_sorted_addrs().collect::>(); + assert!(!addrs.is_empty(), "Cache should contain the added peer"); + assert!( + addrs.iter().any(|&a| a == &addr), + "Cache should contain our specific peer" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_cache_max_peers() -> Result<(), Box> { + let _guard = LogBuilder::init_single_threaded_tokio_test("cache_tests", false); + + let temp_dir = TempDir::new()?; + let cache_path = temp_dir.path().join("cache.json"); + + // Create cache with small max_peers limit + let mut config = BootstrapCacheConfig::empty().with_cache_path(&cache_path); + config.max_peers = 2; + + let mut cache_store = BootstrapCacheStore::new(config)?; + + // Add three peers with distinct timestamps + let mut addresses = Vec::new(); + for i in 1..=3 { + let addr: Multiaddr = format!("/ip4/127.0.0.1/udp/808{}/quic-v1/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UER{}", i, i).parse()?; + addresses.push(addr.clone()); + cache_store.add_addr(addr); + // Add a delay to ensure distinct timestamps + sleep(Duration::from_millis(100)).await; + } + + let addrs = cache_store.get_all_addrs().collect::>(); + assert_eq!(addrs.len(), 2, "Cache should respect max_peers limit"); + + // Get the addresses of the peers we have + let peer_addrs: Vec<_> = addrs.iter().map(|p| p.addr.to_string()).collect(); + tracing::debug!("Final peers: {:?}", peer_addrs); + + // We should have the two most recently added peers (addresses[1] and addresses[2]) + for addr in addrs { + let addr_str = addr.addr.to_string(); + assert!( + addresses[1..].iter().any(|a| a.to_string() == addr_str), + "Should have one of the two most recent peers, got {}", + addr_str + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_cache_file_corruption() -> Result<(), Box> { + let _guard = LogBuilder::init_single_threaded_tokio_test("cache_tests", false); + let temp_dir = TempDir::new()?; + let cache_path = temp_dir.path().join("cache.json"); + + // Create cache with some peers + let config = BootstrapCacheConfig::empty().with_cache_path(&cache_path); + + let mut cache_store = BootstrapCacheStore::new(config.clone())?; + + // Add a peer + let addr: Multiaddr = + "/ip4/127.0.0.1/udp/8080/quic-v1/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UER1" + .parse()?; + cache_store.add_addr(addr.clone()); + + assert_eq!(cache_store.peer_count(), 1); + + // Corrupt the cache file + tokio::fs::write(&cache_path, "invalid json content").await?; + + // Create a new cache store - it should handle the corruption gracefully + let mut new_cache_store = BootstrapCacheStore::new(config)?; + let addrs = new_cache_store.get_all_addrs().collect::>(); + assert!(addrs.is_empty(), "Cache should be empty after corruption"); + + // Should be able to add peers again + new_cache_store.add_addr(addr); + let addrs = new_cache_store.get_all_addrs().collect::>(); + assert_eq!( + addrs.len(), + 1, + "Should be able to add peers after corruption" + ); + + Ok(()) +} diff --git a/ant-bootstrap/tests/cli_integration_tests.rs b/ant-bootstrap/tests/cli_integration_tests.rs new file mode 100644 index 0000000000..1afee9176e --- /dev/null +++ b/ant-bootstrap/tests/cli_integration_tests.rs @@ -0,0 +1,173 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use ant_bootstrap::{BootstrapCacheConfig, PeersArgs}; +use ant_logging::LogBuilder; +use libp2p::Multiaddr; +use tempfile::TempDir; +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +async fn setup() -> (TempDir, BootstrapCacheConfig) { + let temp_dir = TempDir::new().unwrap(); + let cache_path = temp_dir.path().join("cache.json"); + let config = BootstrapCacheConfig::empty().with_cache_path(&cache_path); + + (temp_dir, config) +} + +#[tokio::test] +async fn test_first_flag() -> Result<(), Box> { + let _guard = LogBuilder::init_single_threaded_tokio_test("cli_integration_tests", false); + let (_temp_dir, config) = setup().await; + + let args = PeersArgs { + first: true, + addrs: vec![], + network_contacts_url: None, + local: false, + disable_mainnet_contacts: false, + ignore_cache: false, + }; + + let addrs = args.get_addrs(Some(config)).await?; + + assert!(addrs.is_empty(), "First node should have no addrs"); + + Ok(()) +} + +#[tokio::test] +async fn test_peer_argument() -> Result<(), Box> { + let _guard = LogBuilder::init_single_threaded_tokio_test("cli_integration_tests", false); + let (_temp_dir, _config) = setup().await; + + let peer_addr: Multiaddr = + "/ip4/127.0.0.1/udp/8080/quic-v1/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE" + .parse()?; + + let args = PeersArgs { + first: false, + addrs: vec![peer_addr.clone()], + network_contacts_url: None, + local: false, + disable_mainnet_contacts: true, + ignore_cache: false, + }; + + let addrs = args.get_addrs(None).await?; + + assert_eq!(addrs.len(), 1, "Should have one addr"); + assert_eq!(addrs[0], peer_addr, "Should have the correct address"); + + Ok(()) +} + +#[tokio::test] +async fn test_network_contacts_fallback() -> Result<(), Box> { + let _guard = LogBuilder::init_single_threaded_tokio_test("cli_integration_tests", false); + + let (_temp_dir, config) = setup().await; + + // Start mock server + let mock_server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/peers")) + .respond_with(ResponseTemplate::new(200).set_body_string( + "/ip4/127.0.0.1/udp/8080/quic-v1/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE\n\ + /ip4/127.0.0.2/udp/8081/quic-v1/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERF" + )) + .mount(&mock_server) + .await; + + let args = PeersArgs { + first: false, + addrs: vec![], + network_contacts_url: Some(format!("{}/peers", mock_server.uri()).parse()?), + local: false, + disable_mainnet_contacts: false, + ignore_cache: false, + }; + + let addrs = args.get_addrs(Some(config)).await?; + assert_eq!( + addrs.len(), + 2, + "Should have two peers from network contacts" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_local_mode() -> Result<(), Box> { + let _guard = LogBuilder::init_single_threaded_tokio_test("cli_integration_tests", false); + + let temp_dir = TempDir::new()?; + let cache_path = temp_dir.path().join("cache.json"); + + // Create a config with some peers in the cache + let config = BootstrapCacheConfig::empty().with_cache_path(&cache_path); + + // Create args with local mode enabled + let args = PeersArgs { + first: false, + addrs: vec![], + network_contacts_url: None, + local: true, + disable_mainnet_contacts: false, + ignore_cache: false, + }; + + let addrs = args.get_addrs(Some(config)).await?; + + assert!(addrs.is_empty(), "Local mode should have no peers"); + + // Verify cache was not touched + assert!( + !cache_path.exists(), + "Cache file should not exist in local mode" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_test_network_peers() -> Result<(), Box> { + let _guard = LogBuilder::init_single_threaded_tokio_test("cli_integration_tests", false); + + let temp_dir = TempDir::new()?; + let cache_path = temp_dir.path().join("cache.json"); + + let peer_addr: Multiaddr = + "/ip4/127.0.0.1/udp/8080/quic-v1/p2p/12D3KooWRBhwfeP2Y4TCx1SM6s9rUoHhR5STiGwxBhgFRcw3UERE" + .parse()?; + + let config = BootstrapCacheConfig::empty().with_cache_path(&cache_path); + + let args = PeersArgs { + first: false, + addrs: vec![peer_addr.clone()], + network_contacts_url: None, + local: false, + disable_mainnet_contacts: true, + ignore_cache: false, + }; + + let addrs = args.get_addrs(Some(config)).await?; + + assert_eq!(addrs.len(), 1, "Should have exactly one test network peer"); + assert_eq!( + addrs[0], peer_addr, + "Should have the correct test network peer" + ); + + Ok(()) +} diff --git a/ant-cli/Cargo.toml b/ant-cli/Cargo.toml index 1bad9b6a61..40fa0f182b 100644 --- a/ant-cli/Cargo.toml +++ b/ant-cli/Cargo.toml @@ -15,9 +15,8 @@ path = "src/main.rs" [features] default = ["metrics"] -local = ["ant-peers-acquisition/local", "autonomi/local"] +local = ["ant-bootstrap/local", "autonomi/local"] metrics = ["ant-logging/process-metrics"] -network-contacts = ["ant-peers-acquisition/network-contacts"] websockets = ["autonomi/websockets"] [[bench]] @@ -25,9 +24,9 @@ name = "files" harness = false [dependencies] +ant-bootstrap = { path = "../ant-bootstrap", version = "0.1.0" } ant-build-info = { path = "../ant-build-info", version = "0.1.19" } ant-logging = { path = "../ant-logging", version = "0.2.40" } -ant-peers-acquisition = { path = "../ant-peers-acquisition", version = "0.5.7" } autonomi = { path = "../autonomi", version = "0.2.4", features = [ "fs", "vault", diff --git a/ant-cli/src/access/network.rs b/ant-cli/src/access/network.rs index fb7d5fe597..acf7acfae6 100644 --- a/ant-cli/src/access/network.rs +++ b/ant-cli/src/access/network.rs @@ -6,15 +6,14 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. -use ant_peers_acquisition::PeersArgs; -use ant_peers_acquisition::ANT_PEERS_ENV; +use ant_bootstrap::{PeersArgs, ANT_PEERS_ENV}; use autonomi::Multiaddr; use color_eyre::eyre::Context; use color_eyre::Result; use color_eyre::Section; pub async fn get_peers(peers: PeersArgs) -> Result> { - peers.get_peers().await + peers.get_addrs(None).await .wrap_err("Please provide valid Network peers to connect to") .with_suggestion(|| format!("make sure you've provided network peers using the --peers option or the {ANT_PEERS_ENV} env var")) .with_suggestion(|| "a peer address looks like this: /ip4/42.42.42.42/udp/4242/quic-v1/p2p/B64nodePeerIDvdjb3FAJF4ks3moreBase64CharsHere") diff --git a/ant-cli/src/commands.rs b/ant-cli/src/commands.rs index 663898b6ea..a1d1fd487a 100644 --- a/ant-cli/src/commands.rs +++ b/ant-cli/src/commands.rs @@ -11,11 +11,10 @@ mod register; mod vault; mod wallet; +use crate::opt::Opt; use clap::Subcommand; use color_eyre::Result; -use crate::opt::Opt; - #[derive(Subcommand, Debug)] pub enum SubCmd { /// Operations related to file handling. diff --git a/ant-cli/src/main.rs b/ant-cli/src/main.rs index cbab96d8fc..b50092e538 100644 --- a/ant-cli/src/main.rs +++ b/ant-cli/src/main.rs @@ -51,6 +51,7 @@ async fn main() -> Result<()> { fn init_logging_and_metrics(opt: &Opt) -> Result<(ReloadHandle, Option)> { let logging_targets = vec![ + ("ant_bootstrap".to_string(), Level::DEBUG), ("ant_build_info".to_string(), Level::TRACE), ("ant_evm".to_string(), Level::TRACE), ("ant_networking".to_string(), Level::INFO), @@ -59,7 +60,6 @@ fn init_logging_and_metrics(opt: &Opt) -> Result<(ReloadHandle, Option Result> ("antctl".to_string(), Level::TRACE), ("antctld".to_string(), Level::TRACE), // libs + ("ant_bootstrap".to_string(), Level::TRACE), ("ant_build_info".to_string(), Level::TRACE), ("ant_evm".to_string(), Level::TRACE), ("ant_logging".to_string(), Level::TRACE), ("ant_node_manager".to_string(), Level::TRACE), ("ant_node_rpc_client".to_string(), Level::TRACE), - ("ant_peers_acquisition".to_string(), Level::TRACE), ("ant_protocol".to_string(), Level::TRACE), ("ant_registers".to_string(), Level::INFO), ("ant_service_management".to_string(), Level::TRACE), diff --git a/ant-logging/src/lib.rs b/ant-logging/src/lib.rs index 394e7f1e5a..69f190317b 100644 --- a/ant-logging/src/lib.rs +++ b/ant-logging/src/lib.rs @@ -255,6 +255,8 @@ impl LogBuilder { None => LogOutputDest::Stdout, }; + println!("Logging test at {test_file_name:?} to {output_dest:?}"); + let mut layers = TracingLayers::default(); let _reload_handle = layers diff --git a/ant-networking/Cargo.toml b/ant-networking/Cargo.toml index 98613fabf8..e1a9d7d20c 100644 --- a/ant-networking/Cargo.toml +++ b/ant-networking/Cargo.toml @@ -21,6 +21,7 @@ websockets = ["libp2p/tcp"] [dependencies] aes-gcm-siv = "0.11.1" +ant-bootstrap = { path = "../ant-bootstrap", version = "0.1.0" } ant-build-info = { path = "../ant-build-info", version = "0.1.19" } ant-evm = { path = "../ant-evm", version = "0.1.4" } ant-protocol = { path = "../ant-protocol", version = "0.17.15" } diff --git a/ant-networking/src/driver.rs b/ant-networking/src/driver.rs index 7ab95144f4..872e55d26a 100644 --- a/ant-networking/src/driver.rs +++ b/ant-networking/src/driver.rs @@ -21,15 +21,15 @@ use crate::{ record_store_api::UnifiedRecordStore, relay_manager::RelayManager, replication_fetcher::ReplicationFetcher, + target_arch::Interval, target_arch::{interval, spawn, Instant}, - GetRecordError, Network, CLOSE_GROUP_SIZE, + transport, GetRecordError, Network, NodeIssue, CLOSE_GROUP_SIZE, }; #[cfg(feature = "open-metrics")] use crate::{ metrics::service::run_metrics_server, metrics::NetworkMetricsRecorder, MetricsRegistries, }; -use crate::{transport, NodeIssue}; - +use ant_bootstrap::BootstrapCacheStore; use ant_evm::PaymentQuote; use ant_protocol::{ messages::{ChunkProof, Nonce, Request, Response}, @@ -260,13 +260,13 @@ pub(super) struct NodeBehaviour { #[derive(Debug)] pub struct NetworkBuilder { + bootstrap_cache: Option, is_behind_home_network: bool, keypair: Keypair, local: bool, listen_addr: Option, request_timeout: Option, concurrency_limit: Option, - initial_peers: Vec, #[cfg(feature = "open-metrics")] metrics_registries: Option, #[cfg(feature = "open-metrics")] @@ -278,13 +278,13 @@ pub struct NetworkBuilder { impl NetworkBuilder { pub fn new(keypair: Keypair, local: bool) -> Self { Self { + bootstrap_cache: None, is_behind_home_network: false, keypair, local, listen_addr: None, request_timeout: None, concurrency_limit: None, - initial_peers: Default::default(), #[cfg(feature = "open-metrics")] metrics_registries: None, #[cfg(feature = "open-metrics")] @@ -294,6 +294,10 @@ impl NetworkBuilder { } } + pub fn bootstrap_cache(&mut self, bootstrap_cache: BootstrapCacheStore) { + self.bootstrap_cache = Some(bootstrap_cache); + } + pub fn is_behind_home_network(&mut self, enable: bool) { self.is_behind_home_network = enable; } @@ -310,10 +314,6 @@ impl NetworkBuilder { self.concurrency_limit = Some(concurrency_limit); } - pub fn initial_peers(&mut self, initial_peers: Vec) { - self.initial_peers = initial_peers; - } - /// Set the registries used inside the metrics server. /// Configure the `metrics_server_port` to enable the metrics server. #[cfg(feature = "open-metrics")] @@ -720,6 +720,7 @@ impl NetworkBuilder { close_group: Vec::with_capacity(CLOSE_GROUP_SIZE), peers_in_rt: 0, bootstrap, + bootstrap_cache: self.bootstrap_cache, relay_manager, connected_relay_clients: Default::default(), external_address_manager, @@ -815,6 +816,7 @@ pub struct SwarmDriver { pub(crate) close_group: Vec, pub(crate) peers_in_rt: usize, pub(crate) bootstrap: ContinuousNetworkDiscover, + pub(crate) bootstrap_cache: Option, pub(crate) external_address_manager: Option, pub(crate) relay_manager: Option, /// The peers that are using our relay service. @@ -843,7 +845,7 @@ pub struct SwarmDriver { pub(crate) bootstrap_peers: BTreeMap, HashSet>, // Peers that having live connection to. Any peer got contacted during kad network query // will have live connection established. And they may not appear in the RT. - pub(crate) live_connected_peers: BTreeMap, + pub(crate) live_connected_peers: BTreeMap, /// The list of recently established connections ids. /// This is used to prevent log spamming. pub(crate) latest_established_connection_ids: HashMap, @@ -876,6 +878,24 @@ impl SwarmDriver { let mut set_farthest_record_interval = interval(CLOSET_RECORD_CHECK_INTERVAL); let mut relay_manager_reservation_interval = interval(RELAY_MANAGER_RESERVATION_INTERVAL); + let mut bootstrap_cache_save_interval = self.bootstrap_cache.as_ref().and_then(|cache| { + if cache.config().disable_cache_writing { + None + } else { + // add a variance of 10% to the interval, to avoid all nodes writing to disk at the same time. + let duration = + Self::duration_with_variance(cache.config().min_cache_save_duration, 10); + Some(interval(duration)) + } + }); + if let Some(interval) = bootstrap_cache_save_interval.as_mut() { + interval.tick().await; // first tick completes immediately + info!( + "Bootstrap cache save interval is set to {:?}", + interval.period() + ); + } + // temporarily skip processing IncomingConnectionError swarm event to avoid log spamming let mut previous_incoming_connection_error_event = None; loop { @@ -1005,6 +1025,55 @@ impl SwarmDriver { relay_manager.try_connecting_to_relay(&mut self.swarm, &self.bad_nodes) } }, + Some(()) = Self::conditional_interval(&mut bootstrap_cache_save_interval) => { + let Some(bootstrap_cache) = self.bootstrap_cache.as_mut() else { + continue; + }; + let Some(current_interval) = bootstrap_cache_save_interval.as_mut() else { + continue; + }; + let start = Instant::now(); + + let config = bootstrap_cache.config().clone(); + let mut old_cache = bootstrap_cache.clone(); + + let new = match BootstrapCacheStore::new(config) { + Ok(new) => new, + Err(err) => { + error!("Failed to create a new empty cache: {err}"); + continue; + } + }; + *bootstrap_cache = new; + + // save the cache to disk + spawn(async move { + if let Err(err) = old_cache.sync_and_flush_to_disk(true) { + error!("Failed to save bootstrap cache: {err}"); + } + }); + + if current_interval.period() >= bootstrap_cache.config().max_cache_save_duration { + continue; + } + + // add a variance of 1% to the max interval to avoid all nodes writing to disk at the same time. + let max_cache_save_duration = + Self::duration_with_variance(bootstrap_cache.config().max_cache_save_duration, 1); + + // scale up the interval until we reach the max + let scaled = current_interval.period().as_secs().saturating_mul(bootstrap_cache.config().cache_save_scaling_factor); + let new_duration = Duration::from_secs(std::cmp::min(scaled, max_cache_save_duration.as_secs())); + info!("Scaling up the bootstrap cache save interval to {new_duration:?}"); + + // `Interval` ticks immediately for Tokio, but not for `wasmtimer`, which is used for wasm32. + *current_interval = interval(new_duration); + #[cfg(not(target_arch = "wasm32"))] + current_interval.tick().await; + + trace!("Bootstrap cache synced in {:?}", start.elapsed()); + + }, } } } @@ -1156,13 +1225,35 @@ impl SwarmDriver { info!("Listening on {id:?} with addr: {addr:?}"); Ok(()) } + + /// Returns a new duration that is within +/- variance of the provided duration. + fn duration_with_variance(duration: Duration, variance: u32) -> Duration { + let actual_variance = duration / variance; + let random_adjustment = + Duration::from_secs(rand::thread_rng().gen_range(0..actual_variance.as_secs())); + if random_adjustment.as_secs() % 2 == 0 { + duration - random_adjustment + } else { + duration + random_adjustment + } + } + + /// To tick an optional interval inside tokio::select! without looping forever. + async fn conditional_interval(i: &mut Option) -> Option<()> { + match i { + Some(i) => { + i.tick().await; + Some(()) + } + None => None, + } + } } #[cfg(test)] mod tests { use super::check_and_wipe_storage_dir_if_necessary; - - use std::{fs, io::Read}; + use std::{fs, io::Read, time::Duration}; #[tokio::test] async fn version_file_update() { @@ -1219,4 +1310,18 @@ mod tests { // The storage_dir shall be removed as version_key changed assert!(fs::metadata(storage_dir.clone()).is_err()); } + + #[tokio::test] + async fn test_duration_variance_fn() { + let duration = Duration::from_secs(100); + let variance = 10; + for _ in 0..10000 { + let new_duration = crate::SwarmDriver::duration_with_variance(duration, variance); + if new_duration < duration - duration / variance + || new_duration > duration + duration / variance + { + panic!("new_duration: {new_duration:?} is not within the expected range",); + } + } + } } diff --git a/ant-networking/src/event/kad.rs b/ant-networking/src/event/kad.rs index 5934b11bfa..1af95f9d1d 100644 --- a/ant-networking/src/event/kad.rs +++ b/ant-networking/src/event/kad.rs @@ -242,11 +242,12 @@ impl SwarmDriver { peer, is_new_peer, old_peer, + addresses, .. } => { event_string = "kad_event::RoutingUpdated"; if is_new_peer { - self.update_on_peer_addition(peer); + self.update_on_peer_addition(peer, addresses); // This should only happen once if self.bootstrap.notify_new_peer() { diff --git a/ant-networking/src/event/mod.rs b/ant-networking/src/event/mod.rs index ad44f83da2..ae6e2aefca 100644 --- a/ant-networking/src/event/mod.rs +++ b/ant-networking/src/event/mod.rs @@ -16,7 +16,7 @@ use custom_debug::Debug as CustomDebug; #[cfg(feature = "local")] use libp2p::mdns; use libp2p::{ - kad::{Record, RecordKey, K_VALUE}, + kad::{Addresses, Record, RecordKey, K_VALUE}, request_response::ResponseChannel as PeerResponseChannel, Multiaddr, PeerId, }; @@ -232,7 +232,7 @@ impl SwarmDriver { } /// Update state on addition of a peer to the routing table. - pub(crate) fn update_on_peer_addition(&mut self, added_peer: PeerId) { + pub(crate) fn update_on_peer_addition(&mut self, added_peer: PeerId, addresses: Addresses) { self.peers_in_rt = self.peers_in_rt.saturating_add(1); let n_peers = self.peers_in_rt; info!("New peer added to routing table: {added_peer:?}, now we have #{n_peers} connected peers"); @@ -240,6 +240,12 @@ impl SwarmDriver { #[cfg(feature = "loud")] println!("New peer added to routing table: {added_peer:?}, now we have #{n_peers} connected peers"); + if let Some(bootstrap_cache) = &mut self.bootstrap_cache { + for addr in addresses.iter() { + bootstrap_cache.add_addr(addr.clone()); + } + } + self.log_kbuckets(&added_peer); self.send_event(NetworkEvent::PeerAdded(added_peer, self.peers_in_rt)); diff --git a/ant-networking/src/event/swarm.rs b/ant-networking/src/event/swarm.rs index c5fad1256b..84127c43d3 100644 --- a/ant-networking/src/event/swarm.rs +++ b/ant-networking/src/event/swarm.rs @@ -375,8 +375,17 @@ impl SwarmDriver { let _ = self.live_connected_peers.insert( connection_id, - (peer_id, Instant::now() + Duration::from_secs(60)), + ( + peer_id, + endpoint.get_remote_address().clone(), + Instant::now() + Duration::from_secs(60), + ), ); + + if let Some(bootstrap_cache) = self.bootstrap_cache.as_mut() { + bootstrap_cache.update_addr_status(endpoint.get_remote_address(), true); + } + self.insert_latest_established_connection_ids( connection_id, endpoint.get_remote_address(), @@ -406,7 +415,7 @@ impl SwarmDriver { } => { event_string = "OutgoingConnErr"; warn!("OutgoingConnectionError to {failed_peer_id:?} on {connection_id:?} - {error:?}"); - let _ = self.live_connected_peers.remove(&connection_id); + let connection_details = self.live_connected_peers.remove(&connection_id); self.record_connection_metrics(); // we need to decide if this was a critical error and the peer should be removed from the routing table @@ -509,6 +518,15 @@ impl SwarmDriver { if should_clean_peer { warn!("Tracking issue of {failed_peer_id:?}. Clearing it out for now"); + // Just track failures during outgoing connection with `failed_peer_id` inside the bootstrap cache. + // OutgoingConnectionError without peer_id can happen when dialing multiple addresses of a peer. + // And similarly IncomingConnectionError can happen when a peer has multiple transports/listen addrs. + if let (Some((_, failed_addr, _)), Some(bootstrap_cache)) = + (connection_details, self.bootstrap_cache.as_mut()) + { + bootstrap_cache.update_addr_status(&failed_addr, false); + } + if let Some(dead_peer) = self .swarm .behaviour_mut() @@ -641,7 +659,7 @@ impl SwarmDriver { self.last_connection_pruning_time = Instant::now(); let mut removed_conns = 0; - self.live_connected_peers.retain(|connection_id, (peer_id, timeout_time)| { + self.live_connected_peers.retain(|connection_id, (peer_id, _addr, timeout_time)| { // skip if timeout isn't reached yet if Instant::now() < *timeout_time { diff --git a/ant-node-manager/Cargo.toml b/ant-node-manager/Cargo.toml index 94857697b6..50029846c3 100644 --- a/ant-node-manager/Cargo.toml +++ b/ant-node-manager/Cargo.toml @@ -21,7 +21,6 @@ path = "src/bin/daemon/main.rs" chaos = [] default = ["quic"] local = [] -network-contacts = [] nightly = [] open-metrics = [] otlp = [] @@ -31,10 +30,10 @@ tcp = [] websockets = [] [dependencies] +ant-bootstrap = { path = "../ant-bootstrap", version = "0.1.0" } ant-build-info = { path = "../ant-build-info", version = "0.1.19" } ant-evm = { path = "../ant-evm", version = "0.1.4" } ant-logging = { path = "../ant-logging", version = "0.2.40" } -ant-peers-acquisition = { path = "../ant-peers-acquisition", version = "0.5.7" } ant-protocol = { path = "../ant-protocol", version = "0.17.15" } ant-releases = { git = "https://github.com/jacderida/ant-releases.git", branch = "chore-rename_binaries" } ant-service-management = { path = "../ant-service-management", version = "0.4.3" } diff --git a/ant-node-manager/src/bin/cli/main.rs b/ant-node-manager/src/bin/cli/main.rs index 1e40d20589..14b84e55f7 100644 --- a/ant-node-manager/src/bin/cli/main.rs +++ b/ant-node-manager/src/bin/cli/main.rs @@ -9,6 +9,7 @@ mod subcommands; use crate::subcommands::evm_network::EvmNetworkCommand; +use ant_bootstrap::PeersArgs; use ant_evm::RewardsAddress; use ant_logging::{LogBuilder, LogFormat}; use ant_node_manager::{ @@ -16,7 +17,6 @@ use ant_node_manager::{ cmd::{self}, VerbosityLevel, DEFAULT_NODE_STARTUP_CONNECTION_TIMEOUT_S, }; -use ant_peers_acquisition::PeersArgs; use clap::{Parser, Subcommand}; use color_eyre::{eyre::eyre, Result}; use libp2p::Multiaddr; @@ -131,11 +131,6 @@ pub enum SubCmd { /// This enables the use of antnode services from a home network with a router. #[clap(long)] home_network: bool, - /// Set this flag to launch antnode with the --local flag. - /// - /// This is useful for building a service-based local network. - #[clap(long)] - local: bool, /// Provide the path for the log directory for the installed node. /// /// This path is a prefix. Each installed node will have its own directory underneath it. @@ -1075,7 +1070,6 @@ async fn main() -> Result<()> { env_variables, evm_network, home_network, - local, log_dir_path, log_format, max_archived_log_files, @@ -1103,7 +1097,7 @@ async fn main() -> Result<()> { env_variables, Some(evm_network.try_into()?), home_network, - local, + peers.local, log_dir_path, log_format, max_archived_log_files, @@ -1381,9 +1375,9 @@ async fn main() -> Result<()> { fn get_log_builder(level: Level) -> Result { let logging_targets = vec![ + ("ant_bootstrap".to_string(), level), ("evmlib".to_string(), level), ("evm-testnet".to_string(), level), - ("ant_peers_acquisition".to_string(), level), ("ant_node_manager".to_string(), level), ("antctl".to_string(), level), ("antctld".to_string(), level), diff --git a/ant-node-manager/src/cmd/auditor.rs b/ant-node-manager/src/cmd/auditor.rs index 92061c1e20..764656d3cc 100644 --- a/ant-node-manager/src/cmd/auditor.rs +++ b/ant-node-manager/src/cmd/auditor.rs @@ -10,7 +10,7 @@ use crate::{ config::{self, is_running_as_root}, print_banner, ServiceManager, VerbosityLevel, }; -use ant_peers_acquisition::PeersArgs; +use ant_bootstrap::PeersArgs; use ant_service_management::{auditor::AuditorService, control::ServiceController, NodeRegistry}; use color_eyre::{eyre::eyre, Result}; use std::path::PathBuf; diff --git a/ant-node-manager/src/cmd/faucet.rs b/ant-node-manager/src/cmd/faucet.rs index d598aed62b..053c3727ac 100644 --- a/ant-node-manager/src/cmd/faucet.rs +++ b/ant-node-manager/src/cmd/faucet.rs @@ -10,7 +10,7 @@ use crate::{ config::{self, is_running_as_root}, print_banner, ServiceManager, VerbosityLevel, }; -use ant_peers_acquisition::PeersArgs; +use ant_bootstrap::PeersArgs; use ant_service_management::{control::ServiceController, FaucetService, NodeRegistry}; use color_eyre::{eyre::eyre, Result}; use std::path::PathBuf; diff --git a/ant-node-manager/src/cmd/local.rs b/ant-node-manager/src/cmd/local.rs index f83c6e3d4c..cdf0bd375c 100644 --- a/ant-node-manager/src/cmd/local.rs +++ b/ant-node-manager/src/cmd/local.rs @@ -14,9 +14,9 @@ use crate::{ local::{kill_network, run_network, LocalNetworkOptions}, print_banner, status_report, VerbosityLevel, }; +use ant_bootstrap::PeersArgs; use ant_evm::{EvmNetwork, RewardsAddress}; use ant_logging::LogFormat; -use ant_peers_acquisition::PeersArgs; use ant_releases::{AntReleaseRepoActions, ReleaseType}; use ant_service_management::{ control::ServiceController, get_local_node_registry_path, NodeRegistry, @@ -72,10 +72,10 @@ pub async fn join( // If no peers are obtained we will attempt to join the existing local network, if one // is running. - let peers = match peers_args.get_peers().await { + let peers = match peers_args.get_addrs(None).await { Ok(peers) => Some(peers), Err(err) => match err { - ant_peers_acquisition::error::Error::PeersNotObtained => { + ant_bootstrap::error::Error::NoBootstrapPeersFound => { warn!("PeersNotObtained, peers is set to None"); None } diff --git a/ant-node-manager/src/cmd/mod.rs b/ant-node-manager/src/cmd/mod.rs index 7a77e81678..45138e640d 100644 --- a/ant-node-manager/src/cmd/mod.rs +++ b/ant-node-manager/src/cmd/mod.rs @@ -184,9 +184,6 @@ fn build_binary(bin_type: &ReleaseType) -> Result { if cfg!(feature = "local") { args.extend(["--features", "local"]); } - if cfg!(feature = "network-contacts") { - args.extend(["--features", "network-contacts"]); - } if cfg!(feature = "websockets") { args.extend(["--features", "websockets"]); } diff --git a/ant-node-manager/src/cmd/nat_detection.rs b/ant-node-manager/src/cmd/nat_detection.rs index afe2d442dd..b43238513f 100644 --- a/ant-node-manager/src/cmd/nat_detection.rs +++ b/ant-node-manager/src/cmd/nat_detection.rs @@ -9,7 +9,7 @@ use crate::{ config::get_node_registry_path, helpers::download_and_extract_release, VerbosityLevel, }; -use ant_peers_acquisition::get_peers_from_url; +use ant_bootstrap::ContactsFetcher; use ant_releases::{AntReleaseRepoActions, ReleaseType}; use ant_service_management::{NatDetectionStatus, NodeRegistry}; use color_eyre::eyre::{bail, OptionExt, Result}; @@ -35,7 +35,11 @@ pub async fn run_nat_detection( let servers = match servers { Some(servers) => servers, None => { - let servers = get_peers_from_url(NAT_DETECTION_SERVERS_LIST_URL.parse()?).await?; + let mut contacts_fetcher = ContactsFetcher::new()?; + contacts_fetcher.ignore_peer_id(true); + contacts_fetcher.insert_endpoint(NAT_DETECTION_SERVERS_LIST_URL.parse()?); + + let servers = contacts_fetcher.fetch_addrs().await?; servers .choose_multiple(&mut rand::thread_rng(), 10) diff --git a/ant-node-manager/src/cmd/node.rs b/ant-node-manager/src/cmd/node.rs index 59a04ddc11..d21de2b45e 100644 --- a/ant-node-manager/src/cmd/node.rs +++ b/ant-node-manager/src/cmd/node.rs @@ -18,9 +18,9 @@ use crate::{ helpers::{download_and_extract_release, get_bin_version}, print_banner, refresh_node_registry, status_report, ServiceManager, VerbosityLevel, }; +use ant_bootstrap::PeersArgs; use ant_evm::{EvmNetwork, RewardsAddress}; use ant_logging::LogFormat; -use ant_peers_acquisition::PeersArgs; use ant_releases::{AntReleaseRepoActions, ReleaseType}; use ant_service_management::{ control::{ServiceControl, ServiceController}, @@ -117,13 +117,13 @@ pub async fn add( // If the `antnode` binary we're using has `network-contacts` enabled (which is the case for released binaries), // it's fine if the service definition doesn't call `antnode` with a `--peer` argument. let is_first = peers_args.first; - let bootstrap_peers = match peers_args.get_peers_exclude_network_contacts().await { + let bootstrap_peers = match peers_args.get_addrs(None).await { Ok(peers) => { info!("Obtained peers of length {}", peers.len()); - peers + peers.into_iter().take(10).collect::>() } Err(err) => match err { - ant_peers_acquisition::error::Error::PeersNotObtained => { + ant_bootstrap::error::Error::NoBootstrapPeersFound => { info!("No bootstrap peers obtained, setting empty vec."); Vec::new() } diff --git a/ant-node-rpc-client/Cargo.toml b/ant-node-rpc-client/Cargo.toml index 057ed08492..c34db03215 100644 --- a/ant-node-rpc-client/Cargo.toml +++ b/ant-node-rpc-client/Cargo.toml @@ -19,7 +19,6 @@ nightly = [] [dependencies] ant-build-info = { path = "../ant-build-info", version = "0.1.19" } ant-logging = { path = "../ant-logging", version = "0.2.40" } -ant-peers-acquisition = { path = "../ant-peers-acquisition", version = "0.5.7" } ant-protocol = { path = "../ant-protocol", version = "0.17.15", features=["rpc"] } ant-node = { path = "../ant-node", version = "0.112.6" } ant-service-management = { path = "../ant-service-management", version = "0.4.3" } diff --git a/ant-node/Cargo.toml b/ant-node/Cargo.toml index a1a5700b64..8daa19b30e 100644 --- a/ant-node/Cargo.toml +++ b/ant-node/Cargo.toml @@ -17,10 +17,9 @@ path = "src/bin/antnode/main.rs" default = ["metrics", "upnp", "open-metrics", "encrypt-records"] encrypt-records = ["ant-networking/encrypt-records"] extension-module = ["pyo3/extension-module"] -local = ["ant-networking/local", "ant-evm/local"] +local = ["ant-networking/local", "ant-evm/local", "ant-bootstrap/local"] loud = ["ant-networking/loud"] # loud mode: print important messages to console metrics = ["ant-logging/process-metrics"] -network-contacts = ["ant-peers-acquisition/network-contacts"] nightly = [] open-metrics = ["ant-networking/open-metrics", "prometheus-client"] otlp = ["ant-logging/otlp"] @@ -28,11 +27,11 @@ upnp = ["ant-networking/upnp"] websockets = ["ant-networking/websockets"] [dependencies] +ant-bootstrap = { path = "../ant-bootstrap", version = "0.1.0" } ant-build-info = { path = "../ant-build-info", version = "0.1.19" } ant-evm = { path = "../ant-evm", version = "0.1.4" } ant-logging = { path = "../ant-logging", version = "0.2.40" } ant-networking = { path = "../ant-networking", version = "0.19.5" } -ant-peers-acquisition = { path = "../ant-peers-acquisition", version = "0.5.7" } ant-protocol = { path = "../ant-protocol", version = "0.17.15" } ant-registers = { path = "../ant-registers", version = "0.4.3" } ant-service-management = { path = "../ant-service-management", version = "0.4.3" } diff --git a/ant-node/src/bin/antnode/main.rs b/ant-node/src/bin/antnode/main.rs index cebbc0857c..6246206211 100644 --- a/ant-node/src/bin/antnode/main.rs +++ b/ant-node/src/bin/antnode/main.rs @@ -13,12 +13,12 @@ mod rpc_service; mod subcommands; use crate::subcommands::EvmNetworkCommand; +use ant_bootstrap::{BootstrapCacheConfig, BootstrapCacheStore, PeersArgs}; use ant_evm::{get_evm_network_from_env, EvmNetwork, RewardsAddress}; #[cfg(feature = "metrics")] use ant_logging::metrics::init_metrics; use ant_logging::{Level, LogFormat, LogOutputDest, ReloadHandle}; use ant_node::{Marker, NodeBuilder, NodeEvent, NodeEventsReceiver}; -use ant_peers_acquisition::PeersArgs; use ant_protocol::{ node::get_antnode_root_dir, node_rpc::{NodeCtrl, StopResult}, @@ -172,12 +172,6 @@ struct Opt { #[clap(long)] rpc: Option, - /// Run the node in local mode. - /// - /// When this flag is set, we will not filter out local addresses that we observe. - #[clap(long)] - local: bool, - /// Specify the owner(readable discord user name). #[clap(long)] owner: Option, @@ -271,7 +265,13 @@ fn main() -> Result<()> { init_logging(&opt, keypair.public().to_peer_id())?; let rt = Runtime::new()?; - let bootstrap_peers = rt.block_on(opt.peers.get_peers())?; + let mut bootstrap_cache = BootstrapCacheStore::new_from_peers_args( + &opt.peers, + Some(BootstrapCacheConfig::default_config()?), + )?; + // To create the file before startup if it doesn't exist. + bootstrap_cache.sync_and_flush_to_disk(true)?; + let msg = format!( "Running {} v{}", env!("CARGO_BIN_NAME"), @@ -285,13 +285,17 @@ fn main() -> Result<()> { ant_build_info::git_info() ); - info!("Node started with initial_peers {bootstrap_peers:?}"); + info!( + "Node started with bootstrap cache containing {} peers", + bootstrap_cache.peer_count() + ); // Create a tokio runtime per `run_node` attempt, this ensures // any spawned tasks are closed before we would attempt to run // another process with these args. #[cfg(feature = "metrics")] rt.spawn(init_metrics(std::process::id())); + let initial_peres = rt.block_on(opt.peers.get_addrs(None))?; debug!("Node's owner set to: {:?}", opt.owner); let restart_options = rt.block_on(async move { let mut node_builder = NodeBuilder::new( @@ -299,13 +303,14 @@ fn main() -> Result<()> { rewards_address, evm_network, node_socket_addr, - bootstrap_peers, - opt.local, + opt.peers.local, root_dir, #[cfg(feature = "upnp")] opt.upnp, ); - node_builder.is_behind_home_network = opt.home_network; + node_builder.initial_peers(initial_peres); + node_builder.bootstrap_cache(bootstrap_cache); + node_builder.is_behind_home_network(opt.home_network); #[cfg(feature = "open-metrics")] let mut node_builder = node_builder; // if enable flag is provided or only if the port is specified then enable the server by setting Some() @@ -549,12 +554,12 @@ fn monitor_node_events(mut node_events_rx: NodeEventsReceiver, ctrl_tx: mpsc::Se fn init_logging(opt: &Opt, peer_id: PeerId) -> Result<(String, ReloadHandle, Option)> { let logging_targets = vec![ + ("ant_bootstrap".to_string(), Level::INFO), ("ant_build_info".to_string(), Level::DEBUG), ("ant_evm".to_string(), Level::DEBUG), ("ant_logging".to_string(), Level::DEBUG), ("ant_networking".to_string(), Level::INFO), ("ant_node".to_string(), Level::DEBUG), - ("ant_peers_acquisition".to_string(), Level::DEBUG), ("ant_protocol".to_string(), Level::DEBUG), ("ant_registers".to_string(), Level::DEBUG), ("antnode".to_string(), Level::DEBUG), diff --git a/ant-node/src/node.rs b/ant-node/src/node.rs index c1ea235239..018ef4596a 100644 --- a/ant-node/src/node.rs +++ b/ant-node/src/node.rs @@ -12,6 +12,7 @@ use super::{ #[cfg(feature = "open-metrics")] use crate::metrics::NodeMetricsRecorder; use crate::RunningNode; +use ant_bootstrap::BootstrapCacheStore; use ant_evm::{AttoTokens, RewardsAddress}; #[cfg(feature = "open-metrics")] use ant_networking::MetricsRegistries; @@ -81,41 +82,42 @@ const NETWORK_DENSITY_SAMPLING_INTERVAL_MAX_S: u64 = 200; /// Helper to build and run a Node pub struct NodeBuilder { + bootstrap_cache: Option, + initial_peers: Vec, identity_keypair: Keypair, evm_address: RewardsAddress, evm_network: EvmNetwork, addr: SocketAddr, - initial_peers: Vec, local: bool, root_dir: PathBuf, #[cfg(feature = "open-metrics")] /// Set to Some to enable the metrics server metrics_server_port: Option, /// Enable hole punching for nodes connecting from home networks. - pub is_behind_home_network: bool, + is_behind_home_network: bool, #[cfg(feature = "upnp")] upnp: bool, } impl NodeBuilder { - /// Instantiate the builder - #[expect(clippy::too_many_arguments)] + /// Instantiate the builder. The initial peers can either be supplied via the `initial_peers` method + /// or fetched from the bootstrap cache set using `bootstrap_cache` method. pub fn new( identity_keypair: Keypair, evm_address: RewardsAddress, evm_network: EvmNetwork, addr: SocketAddr, - initial_peers: Vec, local: bool, root_dir: PathBuf, #[cfg(feature = "upnp")] upnp: bool, ) -> Self { Self { + bootstrap_cache: None, + initial_peers: vec![], identity_keypair, evm_address, evm_network, addr, - initial_peers, local, root_dir, #[cfg(feature = "open-metrics")] @@ -132,6 +134,21 @@ impl NodeBuilder { self.metrics_server_port = port; } + /// Set the initialized bootstrap cache. + pub fn bootstrap_cache(&mut self, cache: BootstrapCacheStore) { + self.bootstrap_cache = Some(cache); + } + + /// Set the initial peers to dial at startup. + pub fn initial_peers(&mut self, peers: Vec) { + self.initial_peers = peers; + } + + /// Set the flag to indicate if the node is behind a home network + pub fn is_behind_home_network(&mut self, is_behind_home_network: bool) { + self.is_behind_home_network = is_behind_home_network; + } + /// Asynchronously runs a new node instance, setting up the swarm driver, /// creating a data storage, and handling network events. Returns the /// created `RunningNode` which contains a `NodeEventsChannel` for listening @@ -163,8 +180,10 @@ impl NodeBuilder { network_builder.listen_addr(self.addr); #[cfg(feature = "open-metrics")] network_builder.metrics_server_port(self.metrics_server_port); - network_builder.initial_peers(self.initial_peers.clone()); network_builder.is_behind_home_network(self.is_behind_home_network); + if let Some(cache) = self.bootstrap_cache { + network_builder.bootstrap_cache(cache); + } #[cfg(feature = "upnp")] network_builder.upnp(self.upnp); diff --git a/ant-node/src/python.rs b/ant-node/src/python.rs index 954609b830..3d50520940 100644 --- a/ant-node/src/python.rs +++ b/ant-node/src/python.rs @@ -102,13 +102,13 @@ impl AntNode { rewards_address, evm_network, node_socket_addr, - initial_peers, local, root_dir.unwrap_or_else(|| PathBuf::from(".")), #[cfg(feature = "upnp")] false, ); - node_builder.is_behind_home_network = home_network; + node_builder.initial_peers(initial_peers); + node_builder.is_behind_home_network(home_network); node_builder .build_and_run() diff --git a/ant-peers-acquisition/Cargo.toml b/ant-peers-acquisition/Cargo.toml deleted file mode 100644 index 381f0e0388..0000000000 --- a/ant-peers-acquisition/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -authors = ["MaidSafe Developers "] -description = "Peer acquisition utilities" -edition = "2021" -homepage = "https://maidsafe.net" -license = "GPL-3.0" -name = "ant-peers-acquisition" -readme = "README.md" -repository = "https://github.com/maidsafe/autonomi" -version = "0.5.7" - -[features] -local = [] -network-contacts = ["ant-protocol"] -websockets = [] - -[dependencies] -ant-protocol = { path = "../ant-protocol", version = "0.17.15", optional = true} -clap = { version = "4.2.1", features = ["derive", "env"] } -lazy_static = "~1.4.0" -libp2p = { git = "https://github.com/maqi/rust-libp2p.git", branch = "kad_0.46.2", features = [] } -rand = "0.8.5" -reqwest = { version="0.12.2", default-features=false, features = ["rustls-tls"] } -thiserror = "1.0.23" -tokio = { version = "1.32.0", default-features = false } -tracing = { version = "~0.1.26" } -url = { version = "2.4.0" } - -[lints] -workspace = true diff --git a/ant-peers-acquisition/README.md b/ant-peers-acquisition/README.md deleted file mode 100644 index 6c409a9103..0000000000 --- a/ant-peers-acquisition/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# ant_peers_acquisition - -Provides utilities for discovering bootstrap peers on a given system. - -It handles `--peer` arguments across all bins, as well as `ANT_PEERS` or indeed picking up an initial set of `network-conacts` from a provided, or hard-coded url. diff --git a/ant-peers-acquisition/src/error.rs b/ant-peers-acquisition/src/error.rs deleted file mode 100644 index d5df7c969b..0000000000 --- a/ant-peers-acquisition/src/error.rs +++ /dev/null @@ -1,19 +0,0 @@ -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum Error { - #[error("Could not parse the supplied multiaddr or socket address")] - InvalidPeerAddr(#[from] libp2p::multiaddr::Error), - #[error("Could not obtain network contacts from {0} after {1} retries")] - FailedToObtainPeersFromUrl(String, usize), - #[error("No valid multaddr was present in the contacts file at {0}")] - NoMultiAddrObtainedFromNetworkContacts(String), - #[error("Could not obtain peers through any available options")] - PeersNotObtained, - #[error(transparent)] - ReqwestError(#[from] reqwest::Error), - #[error(transparent)] - UrlParseError(#[from] url::ParseError), -} diff --git a/ant-peers-acquisition/src/lib.rs b/ant-peers-acquisition/src/lib.rs deleted file mode 100644 index da613e97ad..0000000000 --- a/ant-peers-acquisition/src/lib.rs +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -pub mod error; - -use crate::error::{Error, Result}; -use clap::Args; -#[cfg(feature = "network-contacts")] -use lazy_static::lazy_static; -use libp2p::{multiaddr::Protocol, Multiaddr}; -use rand::{seq::SliceRandom, thread_rng}; -use reqwest::Client; -use std::time::Duration; -use tracing::*; -use url::Url; - -#[cfg(feature = "network-contacts")] -lazy_static! { - // URL containing the multi-addresses of the bootstrap nodes. - pub static ref NETWORK_CONTACTS_URL: String = - "https://sn-testnet.s3.eu-west-2.amazonaws.com/network-contacts".to_string(); -} - -// The maximum number of retries to be performed while trying to get peers from a URL. -const MAX_RETRIES_ON_GET_PEERS_FROM_URL: usize = 7; - -/// The name of the environment variable that can be used to pass peers to the node. -pub const ANT_PEERS_ENV: &str = "ANT_PEERS"; - -#[derive(Args, Debug, Default, Clone)] -pub struct PeersArgs { - /// Set to indicate this is the first node in a new network - /// - /// If this argument is used, any others will be ignored because they do not apply to the first - /// node. - #[clap(long)] - pub first: bool, - /// Peer(s) to use for bootstrap, in a 'multiaddr' format containing the peer ID. - /// - /// A multiaddr looks like - /// '/ip4/1.2.3.4/tcp/1200/tcp/p2p/12D3KooWRi6wF7yxWLuPSNskXc6kQ5cJ6eaymeMbCRdTnMesPgFx' where - /// `1.2.3.4` is the IP, `1200` is the port and the (optional) last part is the peer ID. - /// - /// This argument can be provided multiple times to connect to multiple peers. - /// - /// Alternatively, the `ANT_PEERS` environment variable can provide a comma-separated peer - /// list. - #[clap(long = "peer", env = "ANT_PEERS", value_name = "multiaddr", value_delimiter = ',', value_parser = parse_peer_addr, conflicts_with = "first")] - pub peers: Vec, - - /// Specify the URL to fetch the network contacts from. - /// - /// This argument will be overridden if the "peers" argument is set or if the `local` - /// feature flag is enabled. - #[cfg(feature = "network-contacts")] - #[clap(long, conflicts_with = "first")] - pub network_contacts_url: Option, -} - -impl PeersArgs { - /// Gets the peers based on the arguments provided. - /// - /// If the `--first` flag is used, no peers will be provided. - /// - /// Otherwise, peers are obtained in the following order of precedence: - /// * The `--peer` argument. - /// * The `ANT_PEERS` environment variable. - /// * Using the `local` feature, which will return an empty peer list. - /// * Using the `network-contacts` feature, which will download the peer list from a file on S3. - /// - /// Note: the current behaviour is that `--peer` and `ANT_PEERS` will be combined. Some tests - /// currently rely on this. We will change it soon. - pub async fn get_peers(self) -> Result> { - self.get_peers_inner(false).await - } - - /// Gets the peers based on the arguments provided. - /// - /// If the `--first` flag is used, no peers will be provided. - /// - /// Otherwise, peers are obtained in the following order of precedence: - /// * The `--peer` argument. - /// * The `ANT_PEERS` environment variable. - /// * Using the `local` feature, which will return an empty peer list. - /// - /// This will not fetch the peers from network-contacts even if the `network-contacts` feature is enabled. Use - /// get_peers() instead. - /// - /// Note: the current behaviour is that `--peer` and `ANT_PEERS` will be combined. Some tests - /// currently rely on this. We will change it soon. - pub async fn get_peers_exclude_network_contacts(self) -> Result> { - self.get_peers_inner(true).await - } - - async fn get_peers_inner(self, skip_network_contacts: bool) -> Result> { - if self.first { - info!("First node in a new network"); - return Ok(vec![]); - } - - let mut peers = if !self.peers.is_empty() { - info!("Using peers supplied with the --peer argument(s) or ANT_PEERS"); - self.peers - } else if cfg!(feature = "local") { - info!("No peers given"); - info!("The `local` feature is enabled, so peers will be discovered through mDNS."); - return Ok(vec![]); - } else if skip_network_contacts { - info!("Skipping network contacts"); - return Ok(vec![]); - } else if cfg!(feature = "network-contacts") { - self.get_network_contacts().await? - } else { - vec![] - }; - - if peers.is_empty() { - error!("Peers not obtained through any available options"); - return Err(Error::PeersNotObtained); - }; - - // Randomly sort peers before we return them to avoid overly hitting any one peer - let mut rng = thread_rng(); - peers.shuffle(&mut rng); - - Ok(peers) - } - - // should not be reachable, but needed for the compiler to be happy. - #[expect(clippy::unused_async)] - #[cfg(not(feature = "network-contacts"))] - async fn get_network_contacts(&self) -> Result> { - Ok(vec![]) - } - - #[cfg(feature = "network-contacts")] - async fn get_network_contacts(&self) -> Result> { - let url = self - .network_contacts_url - .clone() - .unwrap_or(Url::parse(NETWORK_CONTACTS_URL.as_str())?); - - info!("Trying to fetch the bootstrap peers from {url}"); - - get_peers_from_url(url).await - } -} - -/// Parse strings like `1.2.3.4:1234` and `/ip4/1.2.3.4/tcp/1234` into a multiaddr. -pub fn parse_peer_addr(addr: &str) -> std::result::Result { - // Parse valid IPv4 socket address, e.g. `1.2.3.4:1234`. - if let Ok(addr) = addr.parse::() { - let start_addr = Multiaddr::from(*addr.ip()); - - // Turn the address into a `/ip4//udp//quic-v1` multiaddr. - #[cfg(not(feature = "websockets"))] - let multiaddr = start_addr - .with(Protocol::Udp(addr.port())) - .with(Protocol::QuicV1); - - // Turn the address into a `/ip4//udp//websocket-websys-v1` multiaddr. - #[cfg(feature = "websockets")] - let multiaddr = start_addr - .with(Protocol::Tcp(addr.port())) - .with(Protocol::Ws("/".into())); - - return Ok(multiaddr); - } - - // Parse any valid multiaddr string - addr.parse::() -} - -/// Get and parse a list of peers from a URL. The URL should contain one multiaddr per line. -pub async fn get_peers_from_url(url: Url) -> Result> { - let mut retries = 0; - - #[cfg(not(target_arch = "wasm32"))] - let request_client = Client::builder().timeout(Duration::from_secs(10)).build()?; - // Wasm does not have the timeout method yet. - #[cfg(target_arch = "wasm32")] - let request_client = Client::builder().build()?; - - loop { - let response = request_client.get(url.clone()).send().await; - - match response { - Ok(response) => { - let mut multi_addresses = Vec::new(); - if response.status().is_success() { - let text = response.text().await?; - trace!("Got peers from url: {url}: {text}"); - // example of contacts file exists in resources/network-contacts-examples - for addr in text.split('\n') { - // ignore empty/last lines - if addr.is_empty() { - continue; - } - - debug!("Attempting to parse {addr}"); - multi_addresses.push(parse_peer_addr(addr)?); - } - if !multi_addresses.is_empty() { - trace!("Successfully got peers from URL {multi_addresses:?}"); - return Ok(multi_addresses); - } else { - return Err(Error::NoMultiAddrObtainedFromNetworkContacts( - url.to_string(), - )); - } - } else { - retries += 1; - if retries >= MAX_RETRIES_ON_GET_PEERS_FROM_URL { - return Err(Error::FailedToObtainPeersFromUrl( - url.to_string(), - MAX_RETRIES_ON_GET_PEERS_FROM_URL, - )); - } - } - } - Err(err) => { - error!("Failed to get peers from URL {url}: {err:?}"); - retries += 1; - if retries >= MAX_RETRIES_ON_GET_PEERS_FROM_URL { - return Err(Error::FailedToObtainPeersFromUrl( - url.to_string(), - MAX_RETRIES_ON_GET_PEERS_FROM_URL, - )); - } - } - } - trace!( - "Failed to get peers from URL, retrying {retries}/{MAX_RETRIES_ON_GET_PEERS_FROM_URL}" - ); - tokio::time::sleep(Duration::from_secs(1)).await; - } -} diff --git a/ant-protocol/src/version.rs b/ant-protocol/src/version.rs index 2ead274254..6606e74be0 100644 --- a/ant-protocol/src/version.rs +++ b/ant-protocol/src/version.rs @@ -44,7 +44,7 @@ lazy_static! { // Protocol support shall be downward compatible for patch only version update. // i.e. versions of `A.B.X` or `A.B.X-alpha.Y` shall be considered as a same protocol of `A.B` -fn get_truncate_version_str() -> String { +pub fn get_truncate_version_str() -> String { let version_str = env!("CARGO_PKG_VERSION"); let parts = version_str.split('.').collect::>(); if parts.len() >= 2 { diff --git a/autonomi/Cargo.toml b/autonomi/Cargo.toml index 0e15996c27..d49e087524 100644 --- a/autonomi/Cargo.toml +++ b/autonomi/Cargo.toml @@ -26,9 +26,9 @@ vault = ["registers"] websockets = ["ant-networking/websockets"] [dependencies] +ant-bootstrap = { path = "../ant-bootstrap", version = "0.1.0" } ant-evm = { path = "../ant-evm", version = "0.1.4" } ant-networking = { path = "../ant-networking", version = "0.19.5" } -ant-peers-acquisition = { path = "../ant-peers-acquisition", version = "0.5.7" } ant-protocol = { version = "0.17.15", path = "../ant-protocol" } ant-registers = { path = "../ant-registers", version = "0.4.3" } bip39 = "2.0.0" @@ -62,7 +62,6 @@ xor_name = "5.0.0" [dev-dependencies] alloy = { version = "0.5.3", default-features = false, features = ["std", "reqwest-rustls-tls", "provider-anvil-node", "sol-types", "json", "signers", "contract", "signer-local", "network"] } ant-logging = { path = "../ant-logging", version = "0.2.40" } -ant-peers-acquisition = { path = "../ant-peers-acquisition", version = "0.5.7" } eyre = "0.6.5" sha2 = "0.10.6" # Do not specify the version field. Release process expects even the local dev deps to be published. diff --git a/autonomi/src/client/mod.rs b/autonomi/src/client/mod.rs index 914b01478b..acc62981da 100644 --- a/autonomi/src/client/mod.rs +++ b/autonomi/src/client/mod.rs @@ -30,6 +30,7 @@ pub mod wasm; // private module with utility functions mod utils; +use ant_bootstrap::{BootstrapCacheConfig, BootstrapCacheStore}; pub use ant_evm::Amount; use ant_networking::{interval, multiaddr_is_global, Network, NetworkBuilder, NetworkEvent}; @@ -131,7 +132,16 @@ impl Client { } fn build_client_and_run_swarm(local: bool) -> (Network, mpsc::Receiver) { - let network_builder = NetworkBuilder::new(Keypair::generate_ed25519(), local); + let mut network_builder = NetworkBuilder::new(Keypair::generate_ed25519(), local); + + if let Ok(mut config) = BootstrapCacheConfig::default_config() { + if local { + config.disable_cache_writing = true; + } + if let Ok(cache) = BootstrapCacheStore::new(config) { + network_builder.bootstrap_cache(cache); + } + } // TODO: Re-export `Receiver` from `ant-networking`. Else users need to keep their `tokio` dependency in sync. // TODO: Think about handling the mDNS error here. diff --git a/node-launchpad/Cargo.toml b/node-launchpad/Cargo.toml index 4e488880a2..23926653e0 100644 --- a/node-launchpad/Cargo.toml +++ b/node-launchpad/Cargo.toml @@ -18,10 +18,10 @@ path = "src/bin/tui/main.rs" nightly = [] [dependencies] +ant-bootstrap = { path = "../ant-bootstrap", version = "0.1.0" } ant-build-info = { path = "../ant-build-info", version = "0.1.19" } ant-evm = { path = "../ant-evm", version = "0.1.4" } ant-node-manager = { version = "0.11.3", path = "../ant-node-manager" } -ant-peers-acquisition = { version = "0.5.7", path = "../ant-peers-acquisition" } ant-protocol = { path = "../ant-protocol", version = "0.17.15" } ant-releases = { git = "https://github.com/jacderida/ant-releases.git", branch = "chore-rename_binaries" } ant-service-management = { version = "0.4.3", path = "../ant-service-management" } diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index 40124f4d3f..605c51efd3 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -29,7 +29,7 @@ use crate::{ system::{get_default_mount_point, get_primary_mount_point, get_primary_mount_point_name}, tui, }; -use ant_peers_acquisition::PeersArgs; +use ant_bootstrap::PeersArgs; use color_eyre::eyre::Result; use crossterm::event::KeyEvent; use ratatui::{prelude::Rect, style::Style, widgets::Block}; @@ -317,7 +317,7 @@ impl App { #[cfg(test)] mod tests { use super::*; - use ant_peers_acquisition::PeersArgs; + use ant_bootstrap::PeersArgs; use color_eyre::eyre::Result; use std::io::Cursor; use std::io::Write; diff --git a/node-launchpad/src/bin/tui/main.rs b/node-launchpad/src/bin/tui/main.rs index f2f28af40b..969e2c811a 100644 --- a/node-launchpad/src/bin/tui/main.rs +++ b/node-launchpad/src/bin/tui/main.rs @@ -11,9 +11,9 @@ mod terminal; #[macro_use] extern crate tracing; +use ant_bootstrap::PeersArgs; #[cfg(target_os = "windows")] use ant_node_manager::config::is_running_as_root; -use ant_peers_acquisition::PeersArgs; use clap::Parser; use color_eyre::eyre::Result; use node_launchpad::{ diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 02e39a54ad..1899bbd9bc 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -31,9 +31,9 @@ use crate::{ clear_area, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE, VIVID_SKY_BLUE, }, }; +use ant_bootstrap::PeersArgs; use ant_node_manager::add_services::config::PortRange; use ant_node_manager::config::get_node_registry_path; -use ant_peers_acquisition::PeersArgs; use ant_service_management::{ control::ServiceController, NodeRegistry, NodeServiceData, ServiceStatus, }; diff --git a/node-launchpad/src/node_mgmt.rs b/node-launchpad/src/node_mgmt.rs index 788c2991fa..49fd1c1b32 100644 --- a/node-launchpad/src/node_mgmt.rs +++ b/node-launchpad/src/node_mgmt.rs @@ -1,10 +1,10 @@ use crate::action::{Action, StatusActions}; use crate::connection_mode::ConnectionMode; +use ant_bootstrap::PeersArgs; use ant_evm::{EvmNetwork, RewardsAddress}; use ant_node_manager::{ add_services::config::PortRange, config::get_node_registry_path, VerbosityLevel, }; -use ant_peers_acquisition::PeersArgs; use ant_releases::{self, AntReleaseRepoActions, ReleaseType}; use ant_service_management::NodeRegistry; use color_eyre::eyre::{eyre, Error}; diff --git a/node-launchpad/src/utils.rs b/node-launchpad/src/utils.rs index 15dc6b085e..9defb101e5 100644 --- a/node-launchpad/src/utils.rs +++ b/node-launchpad/src/utils.rs @@ -81,8 +81,12 @@ pub fn initialize_logging() -> Result<()> { .context(format!("Failed to create file {log_path:?}"))?; std::env::set_var( "RUST_LOG", - std::env::var("RUST_LOG") - .unwrap_or_else(|_| format!("{}=trace,ant_node_manager=trace,ant_service_management=trace,ant_peers_acquisition=trace", env!("CARGO_CRATE_NAME"))), + std::env::var("RUST_LOG").unwrap_or_else(|_| { + format!( + "{}=trace,ant_node_manager=trace,ant_service_management=trace,ant_bootstrap=debug", + env!("CARGO_CRATE_NAME") + ) + }), ); let file_subscriber = tracing_subscriber::fmt::layer() .with_file(true) diff --git a/test-utils/Cargo.toml b/test-utils/Cargo.toml index 4d05fbfbb3..4124d37c3e 100644 --- a/test-utils/Cargo.toml +++ b/test-utils/Cargo.toml @@ -9,11 +9,7 @@ readme = "README.md" repository = "https://github.com/maidsafe/safe_network" version = "0.4.11" -[features] -local = ["ant-peers-acquisition/local"] - [dependencies] -ant-peers-acquisition = { path = "../ant-peers-acquisition", version = "0.5.7" } bytes = { version = "1.0.1", features = ["serde"] } color-eyre = "~0.6.2" dirs-next = "~2.0.0" diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index 5d3c57960a..68798d7864 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -9,7 +9,6 @@ pub mod evm; pub mod testnet; -use ant_peers_acquisition::parse_peer_addr; use bytes::Bytes; use color_eyre::eyre::Result; use libp2p::Multiaddr; @@ -39,10 +38,11 @@ pub fn gen_random_data(len: usize) -> Bytes { /// /// An empty `Vec` will be returned if the env var is not set or if local discovery is enabled. pub fn peers_from_env() -> Result> { - let bootstrap_peers = if cfg!(feature = "local") { - Ok(vec![]) - } else if let Some(peers_str) = env_from_runtime_or_compiletime!("ANT_PEERS") { - peers_str.split(',').map(parse_peer_addr).collect() + let bootstrap_peers = if let Some(peers_str) = env_from_runtime_or_compiletime!("ANT_PEERS") { + peers_str + .split(',') + .map(|str| str.parse::()) + .collect() } else { Ok(vec![]) }?;