diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6c43a31d2c1..293445706bb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,7 +12,6 @@ updates: hickory-dns: patterns: - "hickory-*" - - "async-std-resolver" opentelemetry: patterns: - "opentelemetry*" diff --git a/.github/mergify.yml b/.github/mergify.yml index 0d519b38a94..5bbca7d970b 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -1,3 +1,6 @@ +merge_queue: + max_parallel_checks: 1 + pull_request_rules: - name: Ask to resolve conflict conditions: diff --git a/.github/workflows/cache-factory.yml b/.github/workflows/cache-factory.yml index 2176fb937db..4533df0e80d 100644 --- a/.github/workflows/cache-factory.yml +++ b/.github/workflows/cache-factory.yml @@ -18,11 +18,11 @@ jobs: make_stable_rust_cache: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: shared-key: stable-cache diff --git a/.github/workflows/cargo-audit.yml b/.github/workflows/cargo-audit.yml index 405d0a2f799..7a83100f853 100644 --- a/.github/workflows/cargo-audit.yml +++ b/.github/workflows/cargo-audit.yml @@ -8,7 +8,7 @@ jobs: audit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9834ee7423..e217e8b59a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,12 +18,7 @@ env: jobs: test: name: Test ${{ matrix.crate }} - runs-on: ${{ fromJSON( - github.repository == 'libp2p/rust-libp2p' && ( - (contains(fromJSON('["libp2p-webrtc", "libp2p"]'), matrix.crate) && '["self-hosted", "linux", "x64", "2xlarge"]') || - (contains(fromJSON('["libp2p-quic", "libp2p-perf"]'), matrix.crate) && '["self-hosted", "linux", "x64", "xlarge"]') || - '["self-hosted", "linux", "x64", "large"]' - ) || '"ubuntu-latest"') }} + runs-on: ubuntu-latest timeout-minutes: 10 needs: gather_published_crates strategy: @@ -33,7 +28,7 @@ jobs: env: CRATE: ${{ matrix.crate }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 @@ -41,7 +36,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: shared-key: stable-cache save-if: false @@ -109,7 +104,7 @@ jobs: name: Run all WASM tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: @@ -142,7 +137,7 @@ jobs: os: windows-latest runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: @@ -150,7 +145,7 @@ jobs: - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: key: ${{ matrix.target }} save-if: ${{ github.ref == 'refs/heads/master' }} @@ -161,7 +156,7 @@ jobs: name: Compile with MSRV runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Extract MSRV from workspace manifest shell: bash @@ -175,7 +170,7 @@ jobs: - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/master' }} @@ -188,15 +183,14 @@ jobs: matrix: include: - features: "mdns tcp dns tokio" - - features: "mdns tcp dns async-std" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: key: ${{ matrix.features }} save-if: ${{ github.ref == 'refs/heads/master' }} @@ -207,13 +201,13 @@ jobs: name: Check rustdoc intra-doc links runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/master' }} @@ -230,7 +224,7 @@ jobs: beta, ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: @@ -239,7 +233,7 @@ jobs: - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/master' }} @@ -249,13 +243,13 @@ jobs: name: IPFS Integration tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: save-if: ${{ github.ref == 'refs/heads/master' }} @@ -268,13 +262,13 @@ jobs: examples: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: r7kamura/rust-problem-matchers@9fe7ca9f6550e5d6358e179d451cc25ea6b54f98 #v1.5.0 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: shared-key: stable-cache save-if: false @@ -308,14 +302,14 @@ jobs: # https://github.com/obi1kenobi/cargo-semver-checks/issues/589 RUSTFLAGS: '' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: obi1kenobi/cargo-semver-checks-action@v2 - run: cargo semver-checks rustfmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@nightly with: @@ -329,7 +323,7 @@ jobs: manifest_lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable @@ -350,7 +344,7 @@ jobs: outputs: members: ${{ steps.cargo-metadata.outputs.members }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable @@ -363,9 +357,9 @@ jobs: name: Check for changes in proto files runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - run: cargo install --version 0.10.0 pb-rs --locked @@ -390,14 +384,14 @@ jobs: name: Ensure that `Cargo.lock` is up-to-date runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: actions/checkout@v5 + - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 - run: cargo metadata --locked --format-version=1 > /dev/null cargo-deny: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: EmbarkStudios/cargo-deny-action@v2 with: command: check advisories bans licenses sources diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 5cbfc20d69d..103dd3e057b 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -11,7 +11,7 @@ jobs: server: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: docker/login-action@v3 with: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e2bac78c006..f6783ce11f6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install nightly toolchain run: rustup toolchain install nightly - name: Build Documentation @@ -23,7 +23,7 @@ jobs: echo "" > target/doc/index.html cp -r target/doc/* ./host-docs - name: Upload documentation - uses: actions/upload-pages-artifact@v3.0.1 + uses: actions/upload-pages-artifact@v4.0.0 with: path: "host-docs/" diff --git a/.github/workflows/interop-test.yml b/.github/workflows/interop-test.yml index 406bea27205..c5dbe2323ac 100644 --- a/.github/workflows/interop-test.yml +++ b/.github/workflows/interop-test.yml @@ -13,12 +13,12 @@ jobs: run-transport-interop: name: Run transport interoperability tests if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository - runs-on: ${{ fromJSON(github.repository == 'libp2p/rust-libp2p' && '["self-hosted", "linux", "x64", "4xlarge"]' || '"ubuntu-latest"') }} + runs-on: ubuntu-latest strategy: matrix: flavour: [chromium, native] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: docker/setup-buildx-action@v3 @@ -47,7 +47,7 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository runs-on: ${{ fromJSON(github.repository == 'libp2p/rust-libp2p' && '["self-hosted", "linux", "x64", "4xlarge"]' || '"ubuntu-latest"') }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: docker/setup-buildx-action@v3 - name: Build image run: docker buildx build --load -t rust-libp2p-head . -f hole-punching-tests/Dockerfile diff --git a/.gitignore b/.gitignore index eb5a316cbd1..1b2924b6333 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ target +.idea/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 22c82b5f793..2a1ceb8a7e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,18 +52,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy 0.7.35", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -73,12 +61,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "anes" version = "0.1.6" @@ -232,27 +214,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" -[[package]] -name = "async-attributes" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[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-channel" version = "2.3.1" @@ -265,45 +226,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-executor" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.3.1", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", - "once_cell", -] - [[package]] name = "async-io" version = "2.4.0" @@ -329,113 +251,11 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 5.4.0", + "event-listener", "event-listener-strategy", "pin-project-lite", ] -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" -dependencies = [ - "async-channel 2.3.1", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener 5.4.0", - "futures-lite", - "rustix 0.38.44", - "tracing", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "async-signal" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 0.38.44", - "signal-hook-registry", - "slab", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-std" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" -dependencies = [ - "async-attributes", - "async-channel 1.9.0", - "async-global-executor", - "async-io", - "async-lock", - "async-process", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers 0.3.0", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-std-resolver" -version = "0.25.0-alpha.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abef525d07400182ad6d48b3097d8e88a40aed1f3a6e80f221b2b002cfad608" -dependencies = [ - "async-std", - "async-trait", - "futures-io", - "futures-util", - "hickory-resolver", - "pin-utils", - "socket2", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -458,12 +278,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.88" @@ -687,16 +501,26 @@ dependencies = [ ] [[package]] -name = "blocking" -version = "1.6.1" +name = "borsh" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ - "async-channel 2.3.1", - "async-task", - "futures-io", - "futures-lite", - "piper", + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.100", ] [[package]] @@ -716,7 +540,7 @@ dependencies = [ "tokio", "tokio-util", "tower 0.4.13", - "tower-http", + "tower-http 0.5.2", "tracing", "tracing-subscriber", "tracing-wasm", @@ -922,7 +746,7 @@ version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.100", @@ -1066,6 +890,12 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -1160,7 +990,10 @@ dependencies = [ "curve25519-dalek-derive", "digest", "fiat-crypto", + "group", + "rand_core 0.6.4", "rustc_version", + "serde", "subtle", "zeroize", ] @@ -1199,7 +1032,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18e4fdb82bd54a12e42fb58a800dcae6b9e13982238ce2296dc3570b92148e1f" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.100", ] [[package]] @@ -1317,6 +1150,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "doc-comment" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" + [[package]] name = "dtoa" version = "1.0.10" @@ -1355,7 +1194,6 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -1404,7 +1242,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.100", @@ -1459,12 +1297,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[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.4.0" @@ -1482,7 +1314,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.0", + "event-listener", "pin-project-lite", ] @@ -1621,10 +1453,7 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ - "fastrand", "futures-core", - "futures-io", - "parking", "pin-project-lite", ] @@ -1668,7 +1497,7 @@ version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" dependencies = [ - "gloo-timers 0.2.6", + "gloo-timers", "send_wrapper 0.4.0", ] @@ -1795,8 +1624,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] @@ -1811,18 +1640,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "group" version = "0.13.0" @@ -1846,7 +1663,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.9.0", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1871,33 +1688,34 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "ahash", + "foldhash", ] [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hashlink" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1942,11 +1760,10 @@ checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" [[package]] name = "hickory-proto" -version = "0.25.0-alpha.5" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d00147af6310f4392a31680db52a3ed45a2e0f68eb18e8c3fe5537ecc96d9e2" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ - "async-recursion", "async-trait", "cfg-if", "data-encoding", @@ -1958,7 +1775,8 @@ dependencies = [ "ipnet", "once_cell", "rand 0.9.0", - "socket2", + "ring", + "socket2 0.5.9", "thiserror 2.0.12", "tinyvec", "tokio", @@ -1968,9 +1786,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.25.0-alpha.5" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5762f69ebdbd4ddb2e975cd24690bf21fe6b2604039189c26acddbc427f12887" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", @@ -2097,13 +1915,14 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http 1.3.1", "http-body", @@ -2111,6 +1930,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -2131,7 +1951,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 0.26.8", ] [[package]] @@ -2165,22 +1985,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", "http 1.3.1", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.0", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2360,7 +2186,6 @@ dependencies = [ "netlink-proto", "netlink-sys", "rtnetlink", - "smol", "system-configuration", "tokio", "windows 0.53.0", @@ -2399,12 +2224,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.1", ] [[package]] @@ -2467,7 +2292,7 @@ dependencies = [ "serde_json", "thirtyfour", "tokio", - "tower-http", + "tower-http 0.5.2", "tracing", "tracing-subscriber", "wasm-bindgen", @@ -2482,7 +2307,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.9", "widestring", "windows-sys 0.48.0", "winreg", @@ -2517,6 +2342,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.16" @@ -2543,6 +2378,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2627,15 +2471,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -2644,15 +2479,14 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.171" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libp2p" -version = "0.56.0" +version = "0.56.1" dependencies = [ - "async-std", "bytes", "either", "futures", @@ -2674,7 +2508,6 @@ dependencies = [ "libp2p-metrics", "libp2p-mplex", "libp2p-noise", - "libp2p-peer-store", "libp2p-ping", "libp2p-plaintext", "libp2p-pnet", @@ -2702,7 +2535,7 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" -version = "0.5.0" +version = "0.6.0" dependencies = [ "libp2p-core", "libp2p-identity", @@ -2714,7 +2547,7 @@ dependencies = [ [[package]] name = "libp2p-autonat" -version = "0.14.1" +version = "0.15.0" dependencies = [ "async-trait", "asynchronous-codec", @@ -2741,7 +2574,7 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" -version = "0.5.1" +version = "0.6.0" dependencies = [ "libp2p-core", "libp2p-identify", @@ -2757,7 +2590,7 @@ dependencies = [ [[package]] name = "libp2p-core" -version = "0.43.1" +version = "0.43.2" dependencies = [ "either", "fnv", @@ -2783,13 +2616,14 @@ dependencies = [ [[package]] name = "libp2p-dcutr" -version = "0.13.0" +version = "0.14.0" dependencies = [ "asynchronous-codec", "either", "futures", "futures-bounded", "futures-timer", + "hashlink", "libp2p-core", "libp2p-identify", "libp2p-identity", @@ -2799,7 +2633,6 @@ dependencies = [ "libp2p-swarm-test", "libp2p-tcp", "libp2p-yamux", - "lru", "quick-protobuf", "quick-protobuf-codec", "thiserror 2.0.12", @@ -2813,8 +2646,6 @@ dependencies = [ name = "libp2p-dns" version = "0.44.0" dependencies = [ - "async-std", - "async-std-resolver", "async-trait", "futures", "hickory-resolver", @@ -2829,7 +2660,7 @@ dependencies = [ [[package]] name = "libp2p-floodsub" -version = "0.46.1" +version = "0.47.0" dependencies = [ "asynchronous-codec", "bytes", @@ -2849,9 +2680,9 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" -version = "0.49.0" +version = "0.50.0" dependencies = [ - "async-channel 2.3.1", + "async-channel", "asynchronous-codec", "base64", "byteorder", @@ -2905,7 +2736,7 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.11" +version = "0.2.13" dependencies = [ "asn1_der", "bs58", @@ -2925,6 +2756,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "tari_crypto", "thiserror 2.0.12", "tracing", "zeroize", @@ -2932,7 +2764,7 @@ dependencies = [ [[package]] name = "libp2p-kad" -version = "0.47.1" +version = "0.49.0" dependencies = [ "asynchronous-codec", "bytes", @@ -2976,7 +2808,7 @@ dependencies = [ "libp2p-swarm-test", "rand 0.8.5", "smallvec", - "socket2", + "socket2 0.6.0", "tokio", "tracing", "tracing-subscriber", @@ -2984,7 +2816,7 @@ dependencies = [ [[package]] name = "libp2p-memory-connection-limits" -version = "0.4.0" +version = "0.5.0" dependencies = [ "libp2p-core", "libp2p-identify", @@ -3000,7 +2832,7 @@ dependencies = [ [[package]] name = "libp2p-metrics" -version = "0.17.0" +version = "0.17.1" dependencies = [ "futures", "libp2p-core", @@ -3080,12 +2912,12 @@ dependencies = [ name = "libp2p-peer-store" version = "0.1.0" dependencies = [ + "hashlink", "libp2p", "libp2p-core", "libp2p-identity", "libp2p-swarm", "libp2p-swarm-test", - "lru", "serde_json", "tokio", ] @@ -3118,7 +2950,7 @@ dependencies = [ [[package]] name = "libp2p-ping" -version = "0.46.0" +version = "0.47.0" dependencies = [ "futures", "futures-timer", @@ -3190,7 +3022,7 @@ dependencies = [ "rand 0.8.5", "ring", "rustls", - "socket2", + "socket2 0.6.0", "thiserror 2.0.12", "tokio", "tracing", @@ -3199,7 +3031,7 @@ dependencies = [ [[package]] name = "libp2p-relay" -version = "0.20.0" +version = "0.21.1" dependencies = [ "asynchronous-codec", "bytes", @@ -3220,6 +3052,7 @@ dependencies = [ "rand 0.8.5", "static_assertions", "thiserror 2.0.12", + "tokio", "tracing", "tracing-subscriber", "web-time 1.1.0", @@ -3227,7 +3060,7 @@ dependencies = [ [[package]] name = "libp2p-rendezvous" -version = "0.16.1" +version = "0.17.0" dependencies = [ "async-trait", "asynchronous-codec", @@ -3251,7 +3084,7 @@ dependencies = [ [[package]] name = "libp2p-request-response" -version = "0.28.1" +version = "0.29.0" dependencies = [ "anyhow", "async-trait", @@ -3292,7 +3125,7 @@ dependencies = [ [[package]] name = "libp2p-stream" -version = "0.3.0-alpha.1" +version = "0.4.0-alpha" dependencies = [ "futures", "libp2p-core", @@ -3309,13 +3142,13 @@ dependencies = [ name = "libp2p-swarm" version = "0.47.0" dependencies = [ - "async-std", "criterion", "either", "fnv", "futures", "futures-timer", "getrandom 0.2.15", + "hashlink", "libp2p-core", "libp2p-identify", "libp2p-identity", @@ -3325,7 +3158,6 @@ dependencies = [ "libp2p-swarm-derive", "libp2p-swarm-test", "libp2p-yamux", - "lru", "multistream-select", "quickcheck-ext", "rand 0.8.5", @@ -3342,7 +3174,7 @@ dependencies = [ name = "libp2p-swarm-derive" version = "0.35.1" dependencies = [ - "heck", + "heck 0.5.0", "quote", "syn 2.0.100", ] @@ -3365,16 +3197,14 @@ dependencies = [ [[package]] name = "libp2p-tcp" -version = "0.43.0" +version = "0.44.1" dependencies = [ - "async-io", - "async-std", "futures", "futures-timer", "if-watch", "libc", "libp2p-core", - "socket2", + "socket2 0.6.0", "tokio", "tracing", "tracing-subscriber", @@ -3403,9 +3233,8 @@ dependencies = [ [[package]] name = "libp2p-uds" -version = "0.42.0" +version = "0.43.1" dependencies = [ - "async-std", "futures", "libp2p-core", "tempfile", @@ -3415,7 +3244,7 @@ dependencies = [ [[package]] name = "libp2p-upnp" -version = "0.4.1" +version = "0.6.0" dependencies = [ "futures", "futures-timer", @@ -3428,7 +3257,7 @@ dependencies = [ [[package]] name = "libp2p-webrtc" -version = "0.9.0-alpha" +version = "0.9.0-alpha.2" dependencies = [ "async-trait", "futures", @@ -3495,7 +3324,7 @@ dependencies = [ [[package]] name = "libp2p-websocket" -version = "0.45.1" +version = "0.45.2" dependencies = [ "either", "futures", @@ -3513,7 +3342,7 @@ dependencies = [ "tokio", "tracing", "url", - "webpki-roots", + "webpki-roots 0.26.8", ] [[package]] @@ -3536,7 +3365,7 @@ dependencies = [ [[package]] name = "libp2p-webtransport-websys" -version = "0.5.1" +version = "0.5.2" dependencies = [ "futures", "js-sys", @@ -3612,9 +3441,6 @@ name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -dependencies = [ - "value-bag", -] [[package]] name = "loom" @@ -3629,22 +3455,13 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.2", -] - [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -3688,6 +3505,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "metrics-example" version = "0.1.0" @@ -3778,18 +3607,21 @@ dependencies = [ [[package]] name = "multiaddr" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" +version = "0.18.3" dependencies = [ "arrayref", + "bincode", "byteorder", + "bytes", "data-encoding", "libp2p-identity", "multibase", "multihash", "percent-encoding", + "quickcheck", + "rand 0.9.0", "serde", + "serde_json", "static_assertions", "unsigned-varint", "url", @@ -3824,7 +3656,6 @@ dependencies = [ name = "multistream-select" version = "0.13.0" dependencies = [ - "async-std", "bytes", "futures", "futures_ringbuf", @@ -3832,6 +3663,8 @@ dependencies = [ "quickcheck-ext", "rw-stream-sink", "smallvec", + "tokio", + "tokio-util", "tracing", "tracing-subscriber", "unsigned-varint", @@ -3911,7 +3744,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" dependencies = [ - "async-io", "bytes", "futures", "libc", @@ -3959,12 +3791,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -4043,6 +3874,10 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "oorandom" @@ -4108,7 +3943,7 @@ checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a" dependencies = [ "futures-core", "futures-sink", - "indexmap 2.9.0", + "indexmap 2.13.0", "js-sys", "once_cell", "pin-project-lite", @@ -4253,12 +4088,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.13.2" @@ -4385,17 +4214,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -4505,7 +4323,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.24", + "zerocopy", ] [[package]] @@ -4517,6 +4335,15 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -4528,9 +4355,9 @@ dependencies = [ [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c" +checksum = "e4500adecd7af8e0e9f4dbce15cfee07ce913fbf6ad605cc468b83f2d531ee94" dependencies = [ "dtoa", "itoa", @@ -4540,9 +4367,9 @@ dependencies = [ [[package]] name = "prometheus-client-derive-encode" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +checksum = "9adf1691c04c0a5ff46ff8f262b58beb07b0dbb61f96f9f54f6cbd82106ed87f" dependencies = [ "proc-macro2", "quote", @@ -4628,7 +4455,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2", + "socket2 0.5.9", "thiserror 2.0.12", "tokio", "tracing", @@ -4664,7 +4491,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.9", "tracing", "windows-sys 0.59.0", ] @@ -4716,7 +4543,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.24", + "zerocopy", ] [[package]] @@ -4866,17 +4693,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -4887,15 +4705,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -4926,15 +4738,14 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", - "futures-util", "h2", "http 1.3.1", "http-body", @@ -4943,34 +4754,30 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tokio-rustls", "tower 0.5.2", + "tower-http 0.6.7", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", - "windows-registry", + "webpki-roots 1.0.4", ] [[package]] @@ -5054,7 +4861,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a552eb82d19f38c3beed3f786bd23aa434ceb9ac43ab44419ca6d67a7e186c0" dependencies = [ - "async-global-executor", "futures", "log", "netlink-packet-core", @@ -5188,15 +4994,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.11.0" @@ -5350,18 +5147,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -5374,7 +5181,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.13.0", "itoa", "memchr", "ryu", @@ -5514,29 +5321,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] -name = "smol" -version = "2.0.2" +name = "smol_str" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" dependencies = [ - "async-channel 2.3.1", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-net", - "async-process", - "blocking", - "futures-lite", + "serde", ] [[package]] -name = "smol_str" -version = "0.2.2" +name = "snafu" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" dependencies = [ - "serde", + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -5566,6 +5378,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "soketto" version = "0.8.1" @@ -5647,7 +5469,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -5777,6 +5599,62 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" +[[package]] +name = "tari_bulletproofs_plus" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98e43bc4d522de252647e34e72aef4d5e33f845a6acc23fb7b1f4e877a7f9053" +dependencies = [ + "blake2", + "byteorder", + "curve25519-dalek", + "digest", + "ff", + "itertools 0.12.1", + "merlin", + "once_cell", + "rand_core 0.6.4", + "serde", + "sha3", + "thiserror-no-std", + "zeroize", +] + +[[package]] +name = "tari_crypto" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b1ee79be37e04fcdb307b2809e658f19e05f31a2b5263c128fc3a5e2dee16" +dependencies = [ + "blake2", + "borsh", + "curve25519-dalek", + "digest", + "log", + "merlin", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", + "sha3", + "snafu", + "subtle", + "tari_bulletproofs_plus", + "tari_utilities", + "zeroize", +] + +[[package]] +name = "tari_utilities" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539470532a8ca1a8a1aa26f6240586f7d0f7d90ab94ae67f092bcd75a1bb4060" +dependencies = [ + "generic-array", + "snafu", + "subtle", + "zeroize", +] + [[package]] name = "tempfile" version = "3.19.1" @@ -5809,7 +5687,7 @@ dependencies = [ "base64", "futures", "http 1.3.1", - "indexmap 2.9.0", + "indexmap 2.13.0", "parking_lot", "paste", "reqwest", @@ -5876,6 +5754,26 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "thiserror-impl-no-std" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e6318948b519ba6dc2b442a6d0b904ebfb8d411a3ad3e07843615a72249758" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "thiserror-no-std" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3ad459d94dd517257cc96add8a43190ee620011bb6e6cdc82dafd97dfafafea" +dependencies = [ + "thiserror-impl-no-std", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -5987,7 +5885,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.9", "tokio-macros", "windows-sys 0.52.0", ] @@ -6056,8 +5954,8 @@ checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.8", + "toml_edit 0.22.24", ] [[package]] @@ -6069,16 +5967,46 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.13.0", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.8", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ "winnow", ] @@ -6103,7 +6031,7 @@ dependencies = [ "percent-encoding", "pin-project", "prost", - "socket2", + "socket2 0.5.9", "tokio", "tokio-stream", "tower 0.4.13", @@ -6173,6 +6101,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -6267,14 +6213,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -6452,12 +6398,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "value-bag" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" - [[package]] name = "vcpkg" version = "0.2.15" @@ -6664,6 +6604,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webrtc" version = "0.12.0" @@ -6794,7 +6743,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6dfe9686c6c9c51428da4de415cb6ca2dc0591ce2b63212e23fd9cccf0e316b" dependencies = [ "log", - "socket2", + "socket2 0.5.9", "thiserror 1.0.69", "tokio", "webrtc-util", @@ -6921,7 +6870,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -7316,9 +7265,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.6" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -7480,33 +7429,13 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive 0.7.35", -] - [[package]] name = "zerocopy" version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ - "zerocopy-derive 0.8.24", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", + "zerocopy-derive", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5f6059fcbb8..c20bc781dfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,51 +74,50 @@ rust-version = "1.83.0" edition = "2021" [workspace.dependencies] -libp2p = { version = "0.56.0", path = "libp2p" } -libp2p-allow-block-list = { version = "0.5.0", path = "misc/allow-block-list" } -libp2p-autonat = { version = "0.14.1", path = "protocols/autonat" } -libp2p-connection-limits = { version = "0.5.1", path = "misc/connection-limits" } -libp2p-core = { version = "0.43.1", path = "core" } -libp2p-dcutr = { version = "0.13.0", path = "protocols/dcutr" } +libp2p = { version = "0.56.1", path = "libp2p" } +libp2p-allow-block-list = { version = "0.6.0", path = "misc/allow-block-list" } +libp2p-autonat = { version = "0.15.0", path = "protocols/autonat" } +libp2p-connection-limits = { version = "0.6.0", path = "misc/connection-limits" } +libp2p-core = { version = "0.43.2", path = "core" } +libp2p-dcutr = { version = "0.14.0", path = "protocols/dcutr" } libp2p-dns = { version = "0.44.0", path = "transports/dns" } -libp2p-floodsub = { version = "0.46.1", path = "protocols/floodsub" } -libp2p-gossipsub = { version = "0.49.0", path = "protocols/gossipsub" } +libp2p-floodsub = { version = "0.47.0", path = "protocols/floodsub" } +libp2p-gossipsub = { version = "0.50.0", path = "protocols/gossipsub" } libp2p-identify = { version = "0.47.0", path = "protocols/identify" } -libp2p-identity = { version = "0.2.11" } -libp2p-kad = { version = "0.47.1", path = "protocols/kad" } +libp2p-identity = { version = "0.2.13", path = "identity" } +libp2p-kad = { version = "0.49.0", path = "protocols/kad" } libp2p-mdns = { version = "0.48.0", path = "protocols/mdns" } -libp2p-memory-connection-limits = { version = "0.4.0", path = "misc/memory-connection-limits" } -libp2p-metrics = { version = "0.17.0", path = "misc/metrics" } +libp2p-memory-connection-limits = { version = "0.5.0", path = "misc/memory-connection-limits" } +libp2p-metrics = { version = "0.17.1", path = "misc/metrics" } libp2p-mplex = { version = "0.43.1", path = "muxers/mplex" } libp2p-noise = { version = "0.46.1", path = "transports/noise" } libp2p-peer-store = { version = "0.1.0", path = "misc/peer-store" } libp2p-perf = { version = "0.4.0", path = "protocols/perf" } -libp2p-ping = { version = "0.46.0", path = "protocols/ping" } +libp2p-ping = { version = "0.47.0", path = "protocols/ping" } libp2p-plaintext = { version = "0.43.0", path = "transports/plaintext" } libp2p-pnet = { version = "0.26.0", path = "transports/pnet" } libp2p-quic = { version = "0.13.0", path = "transports/quic" } -libp2p-relay = { version = "0.20.0", path = "protocols/relay" } -libp2p-rendezvous = { version = "0.16.1", path = "protocols/rendezvous" } -libp2p-request-response = { version = "0.28.1", path = "protocols/request-response" } +libp2p-relay = { version = "0.21.1", path = "protocols/relay" } +libp2p-rendezvous = { version = "0.17.0", path = "protocols/rendezvous" } +libp2p-request-response = { version = "0.29.0", path = "protocols/request-response" } libp2p-server = { version = "0.12.7", path = "misc/server" } -libp2p-stream = { version = "0.3.0-alpha.1", path = "protocols/stream" } +libp2p-stream = { version = "0.4.0-alpha", path = "protocols/stream" } libp2p-swarm = { version = "0.47.0", path = "swarm" } libp2p-swarm-derive = { version = "=0.35.1", path = "swarm-derive" } # `libp2p-swarm-derive` may not be compatible with different `libp2p-swarm` non-breaking releases. E.g. `libp2p-swarm` might introduce a new enum variant `FromSwarm` (which is `#[non-exhaustive]`) in a non-breaking release. Older versions of `libp2p-swarm-derive` would not forward this enum variant within the `NetworkBehaviour` hierarchy. Thus the version pinning is required. libp2p-swarm-test = { version = "0.6.0", path = "swarm-test" } -libp2p-tcp = { version = "0.43.0", path = "transports/tcp" } +libp2p-tcp = { version = "0.44.1", path = "transports/tcp" } libp2p-tls = { version = "0.6.2", path = "transports/tls" } -libp2p-uds = { version = "0.42.0", path = "transports/uds" } -libp2p-upnp = { version = "0.4.1", path = "protocols/upnp" } -libp2p-webrtc = { version = "0.9.0-alpha", path = "transports/webrtc" } +libp2p-uds = { version = "0.43.1", path = "transports/uds" } +libp2p-upnp = { version = "0.6.0", path = "protocols/upnp" } +libp2p-webrtc = { version = "0.9.0-alpha.2", path = "transports/webrtc" } libp2p-webrtc-utils = { version = "0.4.0", path = "misc/webrtc-utils" } libp2p-webrtc-websys = { version = "0.4.0", path = "transports/webrtc-websys" } -libp2p-websocket = { version = "0.45.1", path = "transports/websocket" } +libp2p-websocket = { version = "0.45.2", path = "transports/websocket" } libp2p-websocket-websys = { version = "0.5.0", path = "transports/websocket-websys" } -libp2p-webtransport-websys = { version = "0.5.1", path = "transports/webtransport-websys" } +libp2p-webtransport-websys = { version = "0.5.2", path = "transports/webtransport-websys" } libp2p-yamux = { version = "0.47.0", path = "muxers/yamux" } # External dependencies -async-std-resolver = { version = "0.25.0-alpha.4", default-features = false } asynchronous-codec = { version = "0.7.0" } env_logger = "0.11" futures = "0.3.30" @@ -126,12 +125,13 @@ futures-bounded = { version = "0.2.4" } futures-rustls = { version = "0.26.0", default-features = false } getrandom = "0.2" if-watch = "3.2.1" -hickory-proto = { version = "0.25.0-alpha.4", default-features = false } -hickory-resolver = { version = "0.25.0-alpha.4", default-features = false } -multiaddr = "0.18.1" +hickory-proto = { version = "0.25.2", default-features = false } +hickory-resolver = { version = "0.25.2", default-features = false } +#multiaddr = "0.18.1" +multiaddr = { path = "multiaddr", version = "0.18.3" } multihash = "0.19.1" multistream-select = { version = "0.13.0", path = "misc/multistream-select" } -prometheus-client = "0.23" +prometheus-client = "0.24" quick-protobuf-codec = { version = "0.3.1", path = "misc/quick-protobuf-codec" } quickcheck = { package = "quickcheck-ext", path = "misc/quickcheck-ext" } rcgen = "0.13" @@ -143,7 +143,7 @@ tracing = "0.1.41" tracing-subscriber = "0.3.19" unsigned-varint = { version = "0.8.0" } web-time = "1.1.0" -hashlink = "0.9.0" +hashlink = "0.10.0" [patch.crates-io] diff --git a/README.md b/README.md index 8840b37f2a9..788ae301048 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ This repository is the central place for Rust development of the [libp2p](https://libp2p.io) spec. +## Tari fork +- Add ristretto/Schnorr identity support +- multiaddr moved into this repo to avoid dependency issues +- update hickory_resolver (support for tokio resolver only) + + ## Getting started - **Main documentation** can be found on https://docs.rs/libp2p. @@ -105,3 +111,4 @@ used by [Polkadot](https://www.parity.io/technologies/polkadot/). - [Swarm NL](https://github.com/algorealmInc/SwarmNL) - A library that makes it easy to configure the networking requirements for any distributed application. - [Taple](https://github.com/opencanarias/taple-core) - Sustainable DLT for asset and process traceability by [OpenCanarias](https://www.opencanarias.com/en/). - [Ceylon](https://github.com/ceylonai/ceylon) - A Multi-Agent System (MAS) Development Framework. +- [Fungi](https://github.com/enbop/fungi) - A platform built for seamless multi-device integration. diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 90d26e75608..45aa3a01a88 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.43.2 + +- Add `*_interop` methods to `PeerRecord` for cross-implementation compatibility with Go and JavaScript libp2p. + - `PeerRecord::new_interop()` - Create peer records using standard format + - `PeerRecord::from_signed_envelope_interop()` - Verify peer records using standard format + + The standard format uses libp2p-peer-record domain and multicodec identifier (0x0301) for interoperability. + Existing methods (`new()`, `from_signed_envelope()`) maintain backward compatibility with legacy Rust libp2p format. + + Use the `*_interop` variants when exchanging peer records with non-Rust libp2p implementations. + + See [PR 6230](https://github.com/libp2p/rust-libp2p/pull/6230). + ## 0.43.1 - Remove `once_cell` dependency. See [PR 5913](https://github.com/libp2p/rust-libp2p/pull/5913) diff --git a/core/Cargo.toml b/core/Cargo.toml index 8c6018adb16..374877281ee 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-core" edition.workspace = true rust-version = { workspace = true } description = "Core traits and structs of libp2p" -version = "0.43.1" +version = "0.43.2" authors = ["Parity Technologies "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" @@ -16,7 +16,8 @@ fnv = "1.0" futures = { workspace = true, features = ["executor", "thread-pool"] } futures-timer = "3" web-time = { workspace = true } -libp2p-identity = { workspace = true, features = ["peerid", "ed25519"] } +#libp2p-identity = { workspace = true, features = ["peerid", "ed25519"] } +libp2p-identity = { path = "../identity", features = ["peerid", "ed25519"] } multiaddr = { workspace = true } multihash = { workspace = true } multistream-select = { workspace = true } diff --git a/core/src/peer_record.rs b/core/src/peer_record.rs index 9c6b7f73f05..3ab880effe4 100644 --- a/core/src/peer_record.rs +++ b/core/src/peer_record.rs @@ -4,13 +4,30 @@ use web_time::SystemTime; use crate::{proto, signed_envelope, signed_envelope::SignedEnvelope, DecodeError, Multiaddr}; -const PAYLOAD_TYPE: &str = "/libp2p/routing-state-record"; -const DOMAIN_SEP: &str = "libp2p-routing-state"; +// Legacy constants for backward compatibility with existing Rust libp2p deployments. +const LEGACY_PAYLOAD_TYPE: &str = "/libp2p/routing-state-record"; +const LEGACY_DOMAIN_SEP: &str = "libp2p-routing-state"; + +// Standard constants for cross-implementation compatibility with Go/JS libp2p. +// Defined in https://github.com/multiformats/multicodec/blob/master/table.csv +// and https://github.com/libp2p/specs/blob/master/RFC/0002-signed-envelopes.md. +const STANDARD_PAYLOAD_TYPE: &[u8] = &[0x03, 0x01]; +const STANDARD_DOMAIN_SEP: &str = "libp2p-peer-record"; /// Represents a peer routing record. /// /// Peer records are designed to be distributable and carry a signature by being wrapped in a signed /// envelope. For more information see RFC0003 of the libp2p specifications: +/// +/// ## Cross-Implementation Compatibility +/// +/// This implementation provides two formats: +/// - **Legacy format** (default methods): Compatible with existing Rust libp2p deployments. +/// - **Standard format** (`*_interop` methods): Compatible with Go and JavaScript implementations. +/// +/// Use the `*_interop` variants (e.g., [`PeerRecord::new_interop`], +/// [`PeerRecord::from_signed_envelope_interop`]) when you need to exchange peer records with +/// non-Rust libp2p implementations. #[derive(Debug, PartialEq, Eq, Clone)] pub struct PeerRecord { peer_id: PeerId, @@ -25,15 +42,43 @@ pub struct PeerRecord { } impl PeerRecord { - /// Attempt to re-construct a [`PeerRecord`] from a [`SignedEnvelope`]. + /// Attempt to re-construct a [`PeerRecord`] from a [`SignedEnvelope`] using legacy format. + /// + /// Uses the legacy routing-state-record format for backward compatibility with existing + /// Rust libp2p deployments. /// /// If this function succeeds, the [`SignedEnvelope`] contained a peer record with a valid /// signature and can hence be considered authenticated. + /// + /// For cross-implementation compatibility with Go/JS libp2p, use + /// [`Self::from_signed_envelope_interop`]. pub fn from_signed_envelope(envelope: SignedEnvelope) -> Result { + Self::from_signed_envelope_impl(envelope, LEGACY_DOMAIN_SEP, LEGACY_PAYLOAD_TYPE.as_bytes()) + } + + /// Attempt to re-construct a [`PeerRecord`] from a [`SignedEnvelope`] using standard interop + /// format. + /// + /// Uses the standard libp2p-peer-record format for cross-implementation compatibility + /// with Go and JavaScript libp2p implementations. + /// + /// If this function succeeds, the [`SignedEnvelope`] contained a peer record with a valid + /// signature and can hence be considered authenticated. + pub fn from_signed_envelope_interop( + envelope: SignedEnvelope, + ) -> Result { + Self::from_signed_envelope_impl(envelope, STANDARD_DOMAIN_SEP, STANDARD_PAYLOAD_TYPE) + } + + fn from_signed_envelope_impl( + envelope: SignedEnvelope, + domain: &str, + payload_type: &[u8], + ) -> Result { use quick_protobuf::MessageRead; let (payload, signing_key) = - envelope.payload_and_signing_key(String::from(DOMAIN_SEP), PAYLOAD_TYPE.as_bytes())?; + envelope.payload_and_signing_key(String::from(domain), payload_type)?; let mut reader = BytesReader::from_bytes(payload); let record = proto::PeerRecord::from_reader(&mut reader, payload).map_err(DecodeError)?; @@ -58,11 +103,43 @@ impl PeerRecord { }) } - /// Construct a new [`PeerRecord`] by authenticating the provided addresses with the given key. + /// Construct a new [`PeerRecord`] by authenticating the provided addresses with the given key + /// using legacy format. + /// + /// Uses the legacy routing-state-record format for backward compatibility with existing + /// Rust libp2p deployments. /// /// This is the same key that is used for authenticating every libp2p connection of your /// application, i.e. what you use when setting up your [`crate::transport::Transport`]. + /// + /// For cross-implementation compatibility with Go/JS libp2p, use [`Self::new_interop`]. pub fn new(key: &Keypair, addresses: Vec) -> Result { + Self::new_impl( + key, + addresses, + LEGACY_DOMAIN_SEP, + LEGACY_PAYLOAD_TYPE.as_bytes(), + ) + } + + /// Construct a new [`PeerRecord`] by authenticating the provided addresses with the given key + /// using standard interop format. + /// + /// Uses the standard libp2p-peer-record format for cross-implementation compatibility + /// with Go and JavaScript libp2p implementations. + /// + /// This is the same key that is used for authenticating every libp2p connection of your + /// application, i.e. what you use when setting up your [`crate::transport::Transport`]. + pub fn new_interop(key: &Keypair, addresses: Vec) -> Result { + Self::new_impl(key, addresses, STANDARD_DOMAIN_SEP, STANDARD_PAYLOAD_TYPE) + } + + fn new_impl( + key: &Keypair, + addresses: Vec, + domain: &str, + payload_type: &[u8], + ) -> Result { use quick_protobuf::MessageWrite; let seq = SystemTime::now() @@ -92,12 +169,8 @@ impl PeerRecord { buf }; - let envelope = SignedEnvelope::new( - key, - String::from(DOMAIN_SEP), - PAYLOAD_TYPE.as_bytes().to_vec(), - payload, - )?; + let envelope = + SignedEnvelope::new(key, String::from(domain), payload_type.to_vec(), payload)?; Ok(Self { peer_id, @@ -154,7 +227,7 @@ mod tests { const HOME: &str = "/ip4/127.0.0.1/tcp/1337"; #[test] - fn roundtrip_envelope() { + fn roundtrip_envelope_legacy() { let key = Keypair::generate_ed25519(); let record = PeerRecord::new(&key, vec![HOME.parse().unwrap()]).unwrap(); @@ -166,7 +239,19 @@ mod tests { } #[test] - fn mismatched_signature() { + fn roundtrip_envelope_interop() { + let key = Keypair::generate_ed25519(); + + let record = PeerRecord::new_interop(&key, vec![HOME.parse().unwrap()]).unwrap(); + + let envelope = record.to_signed_envelope(); + let reconstructed = PeerRecord::from_signed_envelope_interop(envelope).unwrap(); + + assert_eq!(reconstructed, record) + } + + #[test] + fn mismatched_signature_legacy() { use quick_protobuf::MessageWrite; let addr: Multiaddr = HOME.parse().unwrap(); @@ -195,8 +280,8 @@ mod tests { SignedEnvelope::new( &identity_b, - String::from(DOMAIN_SEP), - PAYLOAD_TYPE.as_bytes().to_vec(), + String::from(LEGACY_DOMAIN_SEP), + LEGACY_PAYLOAD_TYPE.as_bytes().to_vec(), payload, ) .unwrap() @@ -207,4 +292,47 @@ mod tests { Err(FromEnvelopeError::MismatchedSignature) )); } + + #[test] + fn mismatched_signature_interop() { + use quick_protobuf::MessageWrite; + + let addr: Multiaddr = HOME.parse().unwrap(); + + let envelope = { + let identity_a = Keypair::generate_ed25519(); + let identity_b = Keypair::generate_ed25519(); + + let payload = { + let record = proto::PeerRecord { + peer_id: identity_a.public().to_peer_id().to_bytes(), + seq: 0, + addresses: vec![proto::AddressInfo { + multiaddr: addr.to_vec(), + }], + }; + + let mut buf = Vec::with_capacity(record.get_size()); + let mut writer = Writer::new(&mut buf); + record + .write_message(&mut writer) + .expect("Encoding to succeed"); + + buf + }; + + SignedEnvelope::new( + &identity_b, + String::from(STANDARD_DOMAIN_SEP), + STANDARD_PAYLOAD_TYPE.to_vec(), + payload, + ) + .unwrap() + }; + + assert!(matches!( + PeerRecord::from_signed_envelope_interop(envelope), + Err(FromEnvelopeError::MismatchedSignature) + )); + } } diff --git a/core/src/transport.rs b/core/src/transport.rs index 58a9c2a9557..de6ec279468 100644 --- a/core/src/transport.rs +++ b/core/src/transport.rs @@ -62,7 +62,7 @@ static NEXT_LISTENER_ID: AtomicUsize = AtomicUsize::new(1); pub enum PortUse { /// Always allocate a new port for the dial. New, - /// Best effor reusing of an existing port. + /// Best effort reusing of an existing port. /// /// If there is no listener present that can be used to dial, a new port is allocated. #[default] diff --git a/deny.toml b/deny.toml index f8485cdb1a3..7a6475b5998 100644 --- a/deny.toml +++ b/deny.toml @@ -39,6 +39,7 @@ allow = [ "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", + "CDLA-Permissive-2.0", "ISC", "MIT", "MPL-2.0", diff --git a/examples/README.md b/examples/README.md index b1fb9f1f104..2e397960b76 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,19 +24,26 @@ Each example includes its own README.md file with specific instructions on how t ## Individual libp2p features -- [Chat](./chat) A basic chat application demonstrating libp2p and the mDNS and Gossipsub protocols. -- [Distributed key-value store](./distributed-key-value-store) A basic key value store demonstrating libp2p and the mDNS and Kademlia protocol. +- [Chat](./chat) A basic chat application demonstrating libp2p and the [mDNS] and [Gossipsub] protocols. +- [Distributed key-value store](./distributed-key-value-store) A basic key value store demonstrating libp2p and the [mDNS] and [Kademlia] protocol. - [File sharing application](./file-sharing) Basic file sharing application with peers either providing or locating and getting files by name. - While obviously showcasing how to build a basic file sharing application with the Kademlia and - Request-Response protocol, the actual goal of this example is **to show how to integrate + While obviously showcasing how to build a basic file sharing application with the [Kademlia] and + [Request-Response] protocol, the actual goal of this example is **to show how to integrate rust-libp2p into a larger application**. -- [IPFS Kademlia](./ipfs-kad) Demonstrates how to perform Kademlia queries on the IPFS network. +- [IPFS Kademlia](./ipfs-kad) Demonstrates how to perform [Kademlia] queries on the [IPFS] network. -- [IPFS Private](./ipfs-private) Implementation using the gossipsub, ping and identify protocols to implement the ipfs private swarms feature. +- [IPFS Private](./ipfs-private) Implementation using the [Gossipsub], ping and identify protocols to implement the IPFS private swarms feature. - [Ping](./ping) Small `ping` clone, sending a ping to a peer, expecting a pong as a response. See [tutorial](../libp2p/src/tutorials/ping.rs) for a step-by-step guide building the example. -- [Rendezvous](./rendezvous) Rendezvous Protocol. See [specs](https://github.com/libp2p/specs/blob/master/rendezvous/README.md). +- [Rendezvous](./rendezvous) [Rendezvous] Protocol. See [specs](https://github.com/libp2p/specs/blob/master/rendezvous/README.md). + +[mDNS]: https://github.com/libp2p/specs/blob/master/discovery/mdns.md +[Gossipsub]: https://github.com/libp2p/specs/tree/master/pubsub/gossipsub +[Kademlia]: https://github.com/libp2p/specs/blob/master/kad-dht/README.md +[Request-Response]: https://en.wikipedia.org/wiki/Request%E2%80%93response +[IPFS]: https://ipfs.tech/ +[Rendezvous]: https://github.com/libp2p/specs/blob/master/rendezvous/README.md diff --git a/examples/browser-webrtc/README.md b/examples/browser-webrtc/README.md index eec2c9c0494..2b53cd4c1d8 100644 --- a/examples/browser-webrtc/README.md +++ b/examples/browser-webrtc/README.md @@ -1,11 +1,11 @@ # Rust-libp2p Browser-Server WebRTC Example This example demonstrates how to use the `libp2p-webrtc-websys` transport library in a browser to ping the WebRTC Server. -It uses [wasm-pack](https://rustwasm.github.io/docs/wasm-pack/) to build the project for use in the browser. +It uses [wasm-pack](https://drager.github.io/wasm-pack/) to build the project for use in the browser. ## Running the example -Ensure you have `wasm-pack` [installed](https://rustwasm.github.io/wasm-pack/). +Ensure you have `wasm-pack` [installed](https://drager.github.io/wasm-pack/). 1. Build the client library: ```shell diff --git a/examples/stream/Cargo.toml b/examples/stream/Cargo.toml index 020ea624b50..79c1ea7380f 100644 --- a/examples/stream/Cargo.toml +++ b/examples/stream/Cargo.toml @@ -12,7 +12,7 @@ release = false anyhow = "1" futures = { workspace = true } libp2p = { path = "../../libp2p", features = [ "tokio", "quic"] } -libp2p-stream = { path = "../../protocols/stream", version = "0.3.0-alpha.1" } +libp2p-stream = { path = "../../protocols/stream", version = "0.4.0-alpha" } rand = "0.8" tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } diff --git a/examples/upnp/src/main.rs b/examples/upnp/src/main.rs index 19de8d773ae..b69600dc836 100644 --- a/examples/upnp/src/main.rs +++ b/examples/upnp/src/main.rs @@ -57,8 +57,11 @@ async fn main() -> Result<(), Box> { loop { match swarm.select_next_some().await { SwarmEvent::NewListenAddr { address, .. } => println!("Listening on {address:?}"), - SwarmEvent::Behaviour(upnp::Event::NewExternalAddr(addr)) => { - println!("New external address: {addr}"); + SwarmEvent::Behaviour(upnp::Event::NewExternalAddr { + external_addr, + local_addr: _, + }) => { + println!("New external address: {external_addr}"); } SwarmEvent::Behaviour(upnp::Event::GatewayNotFound) => { println!("Gateway does not support UPnP"); diff --git a/identity/CHANGELOG.md b/identity/CHANGELOG.md index 81ce7ad718b..430d8263e7a 100644 --- a/identity/CHANGELOG.md +++ b/identity/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.2.13 + +- Turn the `quick-protobuf` dependency optional to only the features which require it. + See [PR 6226](https://github.com/libp2p/rust-libp2p/pull/6226) + +## 0.2.12 + +- Avoid depending on the `rand_core` feature in `ed25519-dalek` crate. + See [PR 6070](https://github.com/libp2p/rust-libp2p/pull/6070) + ## 0.2.11 - Switch from `libsecp256` to `k256` for secp256k1 support. diff --git a/identity/Cargo.toml b/identity/Cargo.toml index 366de74eac5..8320872f19d 100644 --- a/identity/Cargo.toml +++ b/identity/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libp2p-identity" -version = "0.2.11" +version = "0.2.13" edition = "2021" # MUST NOT inherit from workspace because we don't want to publish breaking changes to `libp2p-identity`. description = "Data structures and algorithms for identifying peers in libp2p." rust-version = "1.73.0" # MUST NOT inherit from workspace because we don't want to publish breaking changes to `libp2p-identity`. @@ -15,12 +15,13 @@ categories = ["cryptography"] asn1_der = { version = "0.7.6", optional = true } bs58 = { version = "0.5.1", optional = true } ed25519-dalek = { version = "2.1", optional = true } -hkdf = { version = "0.12.4", optional = true } +tari_crypto = { version = "0.22", optional = true } +hkdf = { version = "0.12", optional = true } k256 = { version = "0.13.4", optional = true, features = ["ecdsa", "arithmetic"] } tracing = { workspace = true } multihash = { version = "0.19.1", optional = true } p256 = { version = "0.13", default-features = false, features = ["ecdsa", "std", "pem"], optional = true } -quick-protobuf = "0.8.1" +quick-protobuf = { version = "0.8.1", optional = true } rand = { version = "0.8", optional = true } sec1 = { version = "0.7", default-features = false, optional = true } serde = { version = "1", optional = true, features = ["derive"] } @@ -32,12 +33,13 @@ zeroize = { version = "1.8", optional = true } ring = { workspace = true, features = ["alloc", "std"], optional = true } [features] -secp256k1 = ["dep:k256", "dep:asn1_der", "dep:sha2", "dep:hkdf", "dep:zeroize"] -ecdsa = ["dep:p256", "dep:zeroize", "dep:sec1", "dep:sha2", "dep:hkdf"] -rsa = ["dep:ring", "dep:asn1_der", "dep:rand", "dep:zeroize"] -ed25519 = ["dep:ed25519-dalek", "dep:zeroize", "dep:sha2", "dep:hkdf"] +secp256k1 = ["dep:k256", "dep:asn1_der", "dep:sha2", "dep:hkdf", "dep:zeroize", "dep:quick-protobuf"] +ecdsa = ["dep:p256", "dep:zeroize", "dep:sec1", "dep:sha2", "dep:hkdf", "dep:quick-protobuf"] +rsa = ["dep:ring", "dep:asn1_der", "dep:rand", "dep:zeroize", "dep:quick-protobuf"] +ed25519 = ["dep:ed25519-dalek", "dep:zeroize", "dep:sha2", "dep:hkdf", "dep:quick-protobuf"] +sr25519 = ["dep:tari_crypto", "dep:zeroize", "dep:sha2", "dep:hkdf", "dep:rand", "dep:quick-protobuf"] peerid = ["dep:multihash", "dep:bs58", "dep:thiserror", "dep:sha2", "dep:hkdf"] -rand = ["dep:rand", "ed25519-dalek?/rand_core"] +rand = ["dep:rand"] [dev-dependencies] quickcheck = { workspace = true } diff --git a/identity/src/ed25519.rs b/identity/src/ed25519.rs index 5a1a53dd4af..ff1ff082306 100644 --- a/identity/src/ed25519.rs +++ b/identity/src/ed25519.rs @@ -184,8 +184,11 @@ impl SecretKey { /// Generate a new Ed25519 secret key. #[cfg(feature = "rand")] pub fn generate() -> SecretKey { - let signing = ed25519::SigningKey::generate(&mut rand::rngs::OsRng); - SecretKey(signing.to_bytes()) + use rand::RngCore as _; + + let mut secret = ed25519::SecretKey::default(); + rand::rngs::OsRng.fill_bytes(&mut secret); + SecretKey(secret) } /// Try to parse an Ed25519 secret key from a byte slice diff --git a/identity/src/error.rs b/identity/src/error.rs index 6e8c4d02caa..025b295d3b6 100644 --- a/identity/src/error.rs +++ b/identity/src/error.rs @@ -44,6 +44,7 @@ impl DecodingError { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] pub(crate) fn failed_to_parse(what: &'static str, source: S) -> Self @@ -64,6 +65,7 @@ impl DecodingError { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] pub(crate) fn bad_protobuf( diff --git a/identity/src/generated/keys.proto b/identity/src/generated/keys.proto index 5fbeaf8f6e0..b0499a9ecdb 100644 --- a/identity/src/generated/keys.proto +++ b/identity/src/generated/keys.proto @@ -7,6 +7,7 @@ enum KeyType { Ed25519 = 1; Secp256k1 = 2; ECDSA = 3; + Sr25519 = 4; } message PublicKey { diff --git a/identity/src/generated/keys_proto.rs b/identity/src/generated/keys_proto.rs index ba15fed5004..2bd1caeca65 100644 --- a/identity/src/generated/keys_proto.rs +++ b/identity/src/generated/keys_proto.rs @@ -11,6 +11,7 @@ use quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result}; use quick_protobuf::sizeofs::*; +use zeroize::{Zeroize, ZeroizeOnDrop}; use super::*; #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -19,6 +20,7 @@ pub enum KeyType { Ed25519 = 1, Secp256k1 = 2, ECDSA = 3, + Sr25519 = 4, } impl Default for KeyType { @@ -34,6 +36,7 @@ impl From for KeyType { 1 => KeyType::Ed25519, 2 => KeyType::Secp256k1, 3 => KeyType::ECDSA, + 4 => KeyType::Sr25519, _ => Self::default(), } } @@ -46,6 +49,7 @@ impl<'a> From<&'a str> for KeyType { "Ed25519" => KeyType::Ed25519, "Secp256k1" => KeyType::Secp256k1, "ECDSA" => KeyType::ECDSA, + "Sr25519" => KeyType::Sr25519, _ => Self::default(), } } diff --git a/identity/src/keypair.rs b/identity/src/keypair.rs index a1bbba00fa9..eff413ba82b 100644 --- a/identity/src/keypair.rs +++ b/identity/src/keypair.rs @@ -22,6 +22,7 @@ feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] use quick_protobuf::{BytesReader, Writer}; @@ -32,6 +33,7 @@ use crate::ecdsa; feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] #[cfg(feature = "ed25519")] @@ -40,16 +42,23 @@ use crate::ed25519; feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] use crate::error::OtherVariantError; +#[cfg(feature = "sr25519")] +use crate::sr25519; #[cfg(any( feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] use crate::proto; +#[cfg(feature = "sr25519")] +use tari_crypto::ristretto::RistrettoPublicKey; + #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] use crate::rsa; #[cfg(feature = "secp256k1")] @@ -83,6 +92,9 @@ pub struct Keypair { #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] enum KeyPairInner { + /// An Sr25519 keypair. + #[cfg(feature = "sr25519")] + Sr25519(sr25519::Keypair), /// An Ed25519 keypair. #[cfg(feature = "ed25519")] Ed25519(ed25519::Keypair), @@ -98,6 +110,14 @@ enum KeyPairInner { } impl Keypair { + /// Generate a new Sr25519 keypair. + #[cfg(all(feature = "sr25519", feature = "rand"))] + pub fn generate_sr25519() -> Keypair { + Keypair { + keypair: KeyPairInner::Sr25519(sr25519::Keypair::generate()), + } + } + /// Generate a new Ed25519 keypair. #[cfg(all(feature = "ed25519", feature = "rand"))] pub fn generate_ed25519() -> Keypair { @@ -127,6 +147,11 @@ impl Keypair { self.try_into() } + #[cfg(feature = "sr25519")] + pub fn try_into_sr25519(self) -> Result { + self.try_into() + } + #[cfg(feature = "secp256k1")] pub fn try_into_secp256k1(self) -> Result { self.try_into() @@ -173,11 +198,22 @@ impl Keypair { }) } + #[cfg(feature = "sr25519")] + pub fn sr25519_from_bytes(bytes: impl AsMut<[u8]>) -> Result { + Ok(Keypair { + keypair: KeyPairInner::Sr25519(sr25519::Keypair::from( + sr25519::SecretKey::try_from_bytes(bytes)?, + )), + }) + } + /// Sign a message using the private key of this keypair, producing /// a signature that can be verified using the corresponding public key. #[allow(unused_variables)] pub fn sign(&self, msg: &[u8]) -> Result, SigningError> { match self.keypair { + #[cfg(feature = "sr25519")] + KeyPairInner::Sr25519(ref pair) => Ok(pair.sign(msg)), #[cfg(feature = "ed25519")] KeyPairInner::Ed25519(ref pair) => Ok(pair.sign(msg)), #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] @@ -192,6 +228,10 @@ impl Keypair { /// Get the public key of this keypair. pub fn public(&self) -> PublicKey { match self.keypair { + #[cfg(feature = "sr25519")] + KeyPairInner::Sr25519(ref pair) => PublicKey { + publickey: PublicKeyInner::Sr25519(pair.public().clone()), + }, #[cfg(feature = "ed25519")] KeyPairInner::Ed25519(ref pair) => PublicKey { publickey: PublicKeyInner::Ed25519(pair.public()), @@ -217,6 +257,7 @@ impl Keypair { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] { @@ -227,6 +268,11 @@ impl Keypair { Type: proto::KeyType::Ed25519, Data: data.to_bytes().to_vec(), }, + #[cfg(feature = "sr25519")] + KeyPairInner::Sr25519(ref data) => proto::PrivateKey { + Type: proto::KeyType::Sr25519, + Data: data.to_bytes().to_vec(), + }, #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] KeyPairInner::Rsa(_) => return Err(DecodingError::encoding_unsupported("RSA")), #[cfg(feature = "secp256k1")] @@ -252,6 +298,7 @@ impl Keypair { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" )))] unreachable!() @@ -264,6 +311,7 @@ impl Keypair { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] { @@ -272,10 +320,19 @@ impl Keypair { let mut reader = BytesReader::from_bytes(bytes); let mut private_key = proto::PrivateKey::from_reader(&mut reader, bytes) .map_err(|e| DecodingError::bad_protobuf("private key bytes", e)) - .map(zeroize::Zeroizing::new)?; + .map(|a| zeroize::Zeroizing::new(a))?; #[allow(unreachable_code)] match private_key.Type { + proto::KeyType::Sr25519 => { + #[cfg(feature = "sr25519")] + return sr25519::Keypair::try_from_bytes(&mut private_key.Data).map(|sk| { + Keypair { + keypair: KeyPairInner::Sr25519(sk), + } + }); + Err(DecodingError::missing_feature("sr25519")) + } proto::KeyType::Ed25519 => { #[cfg(feature = "ed25519")] return ed25519::Keypair::try_from_bytes(&mut private_key.Data).map(|sk| { @@ -321,6 +378,7 @@ impl Keypair { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" )))] unreachable!() @@ -329,6 +387,8 @@ impl Keypair { /// Return a [`KeyType`] of the [`Keypair`]. pub fn key_type(&self) -> KeyType { match self.keypair { + #[cfg(feature = "sr25519")] + KeyPairInner::Sr25519(_) => KeyType::Sr25519, #[cfg(feature = "ed25519")] KeyPairInner::Ed25519(_) => KeyType::Ed25519, #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] @@ -361,6 +421,7 @@ impl Keypair { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] pub fn derive_secret(&self, domain: &[u8]) -> Option<[u8; 32]> { @@ -377,6 +438,7 @@ impl Keypair { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" )))] pub fn derive_secret(&self, _: &[u8]) -> Option<[u8; 32]> { @@ -387,6 +449,8 @@ impl Keypair { #[allow(dead_code)] pub(crate) fn secret(&self) -> Option<[u8; 32]> { match self.keypair { + #[cfg(feature = "sr25519")] + KeyPairInner::Sr25519(ref inner) => Some(inner.secret().to_bytes()), #[cfg(feature = "ed25519")] KeyPairInner::Ed25519(ref inner) => Some(inner.secret().to_bytes()), #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] @@ -423,6 +487,15 @@ impl From for Keypair { } } +#[cfg(feature = "sr25519")] +impl From for Keypair { + fn from(kp: sr25519::Keypair) -> Self { + Keypair { + keypair: KeyPairInner::Sr25519(kp), + } + } +} + #[cfg(feature = "secp256k1")] impl From for Keypair { fn from(kp: secp256k1::Keypair) -> Self { @@ -454,6 +527,27 @@ impl TryInto for Keypair { KeyPairInner::Secp256k1(_) => Err(OtherVariantError::new(crate::KeyType::Secp256k1)), #[cfg(feature = "ecdsa")] KeyPairInner::Ecdsa(_) => Err(OtherVariantError::new(crate::KeyType::Ecdsa)), + #[cfg(feature = "sr25519")] + KeyPairInner::Sr25519(_) => Err(OtherVariantError::new(crate::KeyType::Sr25519)), + } + } +} + +#[cfg(feature = "sr25519")] +impl TryInto for Keypair { + type Error = OtherVariantError; + + fn try_into(self) -> Result { + match self.keypair { + KeyPairInner::Sr25519(inner) => Ok(inner), + #[cfg(feature = "ed25519")] + KeyPairInner::Ed25519(_) => Err(OtherVariantError::new(crate::KeyType::Ed25519)), + #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] + KeyPairInner::Rsa(_) => Err(OtherVariantError::new(crate::KeyType::RSA)), + #[cfg(feature = "secp256k1")] + KeyPairInner::Secp256k1(_) => Err(OtherVariantError::new(crate::KeyType::Secp256k1)), + #[cfg(feature = "ecdsa")] + KeyPairInner::Ecdsa(_) => Err(OtherVariantError::new(crate::KeyType::Ecdsa)), } } } @@ -471,6 +565,8 @@ impl TryInto for Keypair { KeyPairInner::Rsa(_) => Err(OtherVariantError::new(crate::KeyType::RSA)), #[cfg(feature = "secp256k1")] KeyPairInner::Secp256k1(_) => Err(OtherVariantError::new(crate::KeyType::Secp256k1)), + #[cfg(feature = "sr25519")] + KeyPairInner::Sr25519(_) => Err(OtherVariantError::new(crate::KeyType::Sr25519)), } } } @@ -482,6 +578,8 @@ impl TryInto for Keypair { fn try_into(self) -> Result { match self.keypair { KeyPairInner::Secp256k1(inner) => Ok(inner), + #[cfg(feature = "sr25519")] + KeyPairInner::Sr25519(_) => Err(OtherVariantError::new(crate::KeyType::Sr25519)), #[cfg(feature = "ed25519")] KeyPairInner::Ed25519(_) => Err(OtherVariantError::new(crate::KeyType::Ed25519)), #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] @@ -499,6 +597,8 @@ impl TryInto for Keypair { fn try_into(self) -> Result { match self.keypair { KeyPairInner::Rsa(inner) => Ok(inner), + #[cfg(feature = "sr25519")] + KeyPairInner::Sr25519(_) => Err(OtherVariantError::new(crate::KeyType::Sr25519)), #[cfg(feature = "ed25519")] KeyPairInner::Ed25519(_) => Err(OtherVariantError::new(crate::KeyType::Ed25519)), #[cfg(feature = "secp256k1")] @@ -511,6 +611,8 @@ impl TryInto for Keypair { #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub(crate) enum PublicKeyInner { + #[cfg(feature = "sr25519")] + Sr25519(sr25519::PublicKey), /// A public Ed25519 key. #[cfg(feature = "ed25519")] Ed25519(ed25519::PublicKey), @@ -540,6 +642,8 @@ impl PublicKey { #[allow(unused_variables)] pub fn verify(&self, msg: &[u8], sig: &[u8]) -> bool { match self.publickey { + #[cfg(feature = "sr25519")] + PublicKeyInner::Sr25519(ref pk) => pk.verify(msg, sig), #[cfg(feature = "ed25519")] PublicKeyInner::Ed25519(ref pk) => pk.verify(msg, sig), #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] @@ -556,6 +660,11 @@ impl PublicKey { self.try_into() } + #[cfg(feature = "sr25519")] + pub fn try_into_sr25519(self) -> Result { + self.try_into() + } + #[cfg(feature = "secp256k1")] pub fn try_into_secp256k1(self) -> Result { self.try_into() @@ -571,6 +680,14 @@ impl PublicKey { self.try_into() } + #[cfg(feature = "sr25519")] + pub fn is_eq_sr25519(&self, other: &RistrettoPublicKey) -> bool { + match &self.publickey { + PublicKeyInner::Sr25519(key) => key.inner_key() == other, + _ => false, + } + } + /// Encode the public key into a protobuf structure for storage or /// exchange with other nodes. pub fn encode_protobuf(&self) -> Vec { @@ -578,6 +695,7 @@ impl PublicKey { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] { @@ -597,6 +715,7 @@ impl PublicKey { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" )))] unreachable!() @@ -610,6 +729,7 @@ impl PublicKey { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] { @@ -626,6 +746,7 @@ impl PublicKey { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" )))] unreachable!() @@ -640,6 +761,8 @@ impl PublicKey { /// Return a [`KeyType`] of the [`PublicKey`]. pub fn key_type(&self) -> KeyType { match self.publickey { + #[cfg(feature = "sr25519")] + PublicKeyInner::Sr25519(_) => KeyType::Sr25519, #[cfg(feature = "ed25519")] PublicKeyInner::Ed25519(_) => KeyType::Ed25519, #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] @@ -656,6 +779,7 @@ impl PublicKey { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] impl TryFrom for PublicKey { @@ -663,6 +787,17 @@ impl TryFrom for PublicKey { fn try_from(pubkey: proto::PublicKey) -> Result { match pubkey.Type { + #[cfg(feature = "sr25519")] + proto::KeyType::Sr25519 => Ok(sr25519::PublicKey::try_from_bytes(&pubkey.Data).map( + |kp| PublicKey { + publickey: PublicKeyInner::Sr25519(kp), + }, + )?), + #[cfg(not(feature = "sr25519"))] + proto::KeyType::Sr25519 => { + tracing::debug!("support for sr25519 was disabled at compile-time"); + Err(DecodingError::missing_feature("sr25519")) + } #[cfg(feature = "ed25519")] proto::KeyType::Ed25519 => Ok(ed25519::PublicKey::try_from_bytes(&pubkey.Data).map( |kp| PublicKey { @@ -725,6 +860,27 @@ impl TryInto for PublicKey { PublicKeyInner::Secp256k1(_) => Err(OtherVariantError::new(crate::KeyType::Secp256k1)), #[cfg(feature = "ecdsa")] PublicKeyInner::Ecdsa(_) => Err(OtherVariantError::new(crate::KeyType::Ecdsa)), + #[cfg(feature = "sr25519")] + PublicKeyInner::Sr25519(_) => Err(OtherVariantError::new(crate::KeyType::Sr25519)), + } + } +} + +#[cfg(feature = "sr25519")] +impl TryInto for PublicKey { + type Error = OtherVariantError; + + fn try_into(self) -> Result { + match self.publickey { + PublicKeyInner::Sr25519(inner) => Ok(inner), + #[cfg(feature = "ed25519")] + PublicKeyInner::Ed25519(_) => Err(OtherVariantError::new(crate::KeyType::Ed25519)), + #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] + PublicKeyInner::Rsa(_) => Err(OtherVariantError::new(crate::KeyType::RSA)), + #[cfg(feature = "secp256k1")] + PublicKeyInner::Secp256k1(_) => Err(OtherVariantError::new(crate::KeyType::Secp256k1)), + #[cfg(feature = "ecdsa")] + PublicKeyInner::Ecdsa(_) => Err(OtherVariantError::new(crate::KeyType::Ecdsa)), } } } @@ -738,6 +894,8 @@ impl TryInto for PublicKey { PublicKeyInner::Ecdsa(inner) => Ok(inner), #[cfg(feature = "ed25519")] PublicKeyInner::Ed25519(_) => Err(OtherVariantError::new(crate::KeyType::Ed25519)), + #[cfg(feature = "sr25519")] + PublicKeyInner::Sr25519(_) => Err(OtherVariantError::new(crate::KeyType::Sr25519)), #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] PublicKeyInner::Rsa(_) => Err(OtherVariantError::new(crate::KeyType::RSA)), #[cfg(feature = "secp256k1")] @@ -755,6 +913,8 @@ impl TryInto for PublicKey { PublicKeyInner::Secp256k1(inner) => Ok(inner), #[cfg(feature = "ed25519")] PublicKeyInner::Ed25519(_) => Err(OtherVariantError::new(crate::KeyType::Ed25519)), + #[cfg(feature = "sr25519")] + PublicKeyInner::Sr25519(_) => Err(OtherVariantError::new(crate::KeyType::Sr25519)), #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] PublicKeyInner::Rsa(_) => Err(OtherVariantError::new(crate::KeyType::RSA)), #[cfg(feature = "ecdsa")] @@ -772,6 +932,8 @@ impl TryInto for PublicKey { PublicKeyInner::Rsa(inner) => Ok(inner), #[cfg(feature = "ed25519")] PublicKeyInner::Ed25519(_) => Err(OtherVariantError::new(crate::KeyType::Ed25519)), + #[cfg(feature = "sr25519")] + PublicKeyInner::Sr25519(_) => Err(OtherVariantError::new(crate::KeyType::Sr25519)), #[cfg(feature = "secp256k1")] PublicKeyInner::Secp256k1(_) => Err(OtherVariantError::new(crate::KeyType::Secp256k1)), #[cfg(feature = "ecdsa")] @@ -789,6 +951,15 @@ impl From for PublicKey { } } +#[cfg(feature = "sr25519")] +impl From for PublicKey { + fn from(key: sr25519::PublicKey) -> Self { + PublicKey { + publickey: PublicKeyInner::Sr25519(key), + } + } +} + #[cfg(feature = "secp256k1")] impl From for PublicKey { fn from(key: secp256k1::PublicKey) -> Self { diff --git a/identity/src/lib.rs b/identity/src/lib.rs index 4f4313e8f17..f51e2f6c32b 100644 --- a/identity/src/lib.rs +++ b/identity/src/lib.rs @@ -39,6 +39,7 @@ feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] mod proto { @@ -52,6 +53,9 @@ pub mod ecdsa; #[cfg(feature = "ed25519")] pub mod ed25519; +#[cfg(feature = "sr25519")] +pub mod sr25519; + #[cfg(all(feature = "rsa", not(target_arch = "wasm32")))] pub mod rsa; @@ -67,6 +71,7 @@ mod peer_id; feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] impl zeroize::Zeroize for proto::PrivateKey { @@ -79,11 +84,17 @@ impl zeroize::Zeroize for proto::PrivateKey { feature = "ecdsa", feature = "secp256k1", feature = "ed25519", + feature = "sr25519", feature = "rsa" ))] impl From<&PublicKey> for proto::PublicKey { fn from(key: &PublicKey) -> Self { match &key.publickey { + #[cfg(feature = "sr25519")] + keypair::PublicKeyInner::Sr25519(key) => proto::PublicKey { + Type: proto::KeyType::Sr25519, + Data: key.to_bytes().to_vec(), + }, #[cfg(feature = "ed25519")] keypair::PublicKeyInner::Ed25519(key) => proto::PublicKey { Type: proto::KeyType::Ed25519, @@ -117,6 +128,7 @@ pub use peer_id::{ParseError, PeerId}; #[derive(Debug, PartialEq, Eq)] #[allow(clippy::upper_case_acronyms)] pub enum KeyType { + Sr25519, Ed25519, RSA, Secp256k1, @@ -126,6 +138,7 @@ pub enum KeyType { impl std::fmt::Display for KeyType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + KeyType::Sr25519 => f.write_str("Sr25519"), KeyType::Ed25519 => f.write_str("Ed25519"), KeyType::RSA => f.write_str("RSA"), KeyType::Secp256k1 => f.write_str("Secp256k1"), diff --git a/identity/src/sr25519.rs b/identity/src/sr25519.rs new file mode 100644 index 00000000000..1d18827d0ff --- /dev/null +++ b/identity/src/sr25519.rs @@ -0,0 +1,281 @@ +// Copyright 2019 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Sr25519 keys. + +use super::error::DecodingError; +use core::fmt; +use std::fmt::{Display, Formatter}; +use tari_crypto::keys::{PublicKey as _, SecretKey as _}; +use tari_crypto::ristretto::{RistrettoPublicKey, RistrettoSchnorr, RistrettoSecretKey}; +use tari_crypto::tari_utilities::ByteArray; +use zeroize::Zeroize; + +/// An Sr25519 keypair. +#[derive(Clone)] +pub struct Keypair { + public: PublicKey, + secret: SecretKey, +} + +impl Keypair { + /// Generate a new random Sr25519 keypair. + #[cfg(feature = "rand")] + pub fn generate() -> Keypair { + Keypair::from(SecretKey::generate()) + } + + /// Convert the keypair into a byte array. This is encoded as the secret key only since the public key can be derived from it. + /// TODO: this leaks the secret key in stack memory + pub fn to_bytes(&self) -> [u8; 32] { + self.secret.to_bytes() + } + + /// Try to parse a keypair from the [binary format](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.5) + /// produced by [`Keypair::to_bytes`], zeroing the input on success. + /// + /// Note that this binary format is the same as `curve25519_dalek`'s compressed bytes + pub fn try_from_bytes(kp: &mut [u8]) -> Result { + let secret = SecretKey::try_from_bytes(kp) + .map_err(|e| DecodingError::failed_to_parse("Sr25519 keypair", e))?; + + Ok(Self::from(secret)) + } + + /// Sign a message using the private key of this keypair. + pub fn sign(&self, msg: &[u8]) -> Vec { + let sig = RistrettoSchnorr::sign(&self.secret.0, msg, &mut rand::rngs::OsRng).expect( + "SchnorrSignature::sign shouldn't return a Result (Blake2b is hard coded as the hasher)", + ); + let mut buf = vec![0u8; 64]; + buf[..32].copy_from_slice(sig.get_public_nonce().as_bytes()); + buf[32..].copy_from_slice(sig.get_signature().as_bytes()); + buf + } + + /// Get the public key of this keypair. + pub fn public(&self) -> &PublicKey { + &self.public + } + + /// Get the secret key of this keypair. + pub fn secret(&self) -> &SecretKey { + &self.secret + } +} + +impl fmt::Debug for Keypair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Keypair") + .field("public", self.public()) + .finish() + } +} + +/// Demote an Sr25519 keypair to a secret key. +impl From for SecretKey { + fn from(kp: Keypair) -> SecretKey { + kp.secret + } +} + +/// Promote an Sr25519 secret key into a keypair. +impl From for Keypair { + fn from(secret: SecretKey) -> Keypair { + Keypair { + public: PublicKey(RistrettoPublicKey::from_secret_key(&secret.0)), + secret, + } + } +} + +/// An Sr25519 public key. +#[derive(Eq, Clone, PartialEq, Hash, PartialOrd, Ord)] +pub struct PublicKey(RistrettoPublicKey); + +impl fmt::Debug for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("PublicKey(compressed): ")?; + for byte in self.0.as_bytes() { + write!(f, "{byte:x}")?; + } + Ok(()) + } +} + +impl PublicKey { + /// Verify the Sr25519 signature on a message using the public key. + pub fn verify(&self, msg: &[u8], sig: &[u8]) -> bool { + if sig.len() != 64 { + return false; + } + let nonce = match RistrettoPublicKey::from_canonical_bytes(&sig[..32]) { + Ok(n) => n, + Err(_) => return false, + }; + let sig = match RistrettoSecretKey::from_canonical_bytes(&sig[32..]) { + Ok(s) => s, + Err(_) => return false, + }; + RistrettoSchnorr::new(nonce, sig).verify(&self.0, msg) + } + + /// Convert the public key to a byte array in compressed form, i.e. + /// where one coordinate is represented by a single bit. + pub fn to_bytes(&self) -> [u8; 32] { + let mut buf = [0u8; 32]; + buf.copy_from_slice(self.0.as_bytes()); + buf + } + + /// Try to parse a public key from a byte array containing the actual key as produced by `to_bytes`. + pub fn try_from_bytes(k: &[u8]) -> Result { + let pk = RistrettoPublicKey::from_canonical_bytes(k) + // TODO: cant pass ByteArrayError because it doesnt implement std Error + .map_err(|e| DecodingError::failed_to_parse("Sr25519 public key", ByteArrayError(e)))?; + Ok(PublicKey(pk)) + } + + pub fn inner_key(&self) -> &RistrettoPublicKey { + &self.0 + } +} + +impl From for PublicKey { + fn from(pk: RistrettoPublicKey) -> Self { + PublicKey(pk) + } +} + +/// An Sr25519 secret key. +#[derive(Clone)] +pub struct SecretKey(RistrettoSecretKey); + +/// View the bytes of the secret key. +impl AsRef<[u8]> for SecretKey { + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl fmt::Debug for SecretKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SecretKey") + } +} + +impl SecretKey { + /// Generate a new Sr25519 secret key. + #[cfg(feature = "rand")] + pub fn generate() -> SecretKey { + SecretKey(RistrettoSecretKey::random(&mut rand::rngs::OsRng)) + } + + /// Try to parse an Sr25519 secret key from a byte slice + /// containing the actual key, zeroing the input on success. + /// If the bytes do not constitute a valid Sr25519 secret key, an error is + /// returned. + pub fn try_from_bytes(mut sk_bytes: impl AsMut<[u8]>) -> Result { + let sk_bytes = sk_bytes.as_mut(); + let secret = RistrettoSecretKey::from_canonical_bytes(&*sk_bytes) + .map_err(|e| DecodingError::failed_to_parse("Sr25519 secret key", ByteArrayError(e)))?; + sk_bytes.zeroize(); + Ok(SecretKey(secret)) + } + + pub fn inner_key(&self) -> &RistrettoSecretKey { + &self.0 + } + + // Not great, leaves the secret key in stack memory (all key types not just Sr25519) + pub(crate) fn to_bytes(&self) -> [u8; 32] { + let mut buf = [0u8; 32]; + buf.copy_from_slice(self.0.as_bytes()); + buf + } +} + +impl From for SecretKey { + fn from(value: RistrettoSecretKey) -> Self { + Self(value) + } +} +#[derive(Debug)] +struct ByteArrayError(tari_crypto::tari_utilities::ByteArrayError); + +impl Display for ByteArrayError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "ByteArrayError: {}", self.0) + } +} + +impl std::error::Error for ByteArrayError {} + +#[cfg(test)] +mod tests { + use super::*; + use quickcheck::*; + + fn eq_keypairs(kp1: &Keypair, kp2: &Keypair) -> bool { + kp1.public() == kp2.public() && kp1.secret.to_bytes() == kp2.secret.to_bytes() + } + + #[test] + #[cfg(feature = "rand")] + fn sr25519_keypair_encode_decode() { + fn prop() -> bool { + let kp1 = Keypair::generate(); + let mut kp1_enc = kp1.to_bytes(); + let kp2 = Keypair::try_from_bytes(&mut kp1_enc).unwrap(); + eq_keypairs(&kp1, &kp2) && kp1_enc.iter().all(|b| *b == 0) + } + QuickCheck::new().tests(10).quickcheck(prop as fn() -> _); + } + + #[test] + #[cfg(feature = "rand")] + fn sr25519_keypair_from_secret() { + fn prop() -> bool { + let kp1 = Keypair::generate(); + let mut sk = kp1.secret.to_bytes(); + let kp2 = Keypair::from(SecretKey::try_from_bytes(&mut sk).unwrap()); + eq_keypairs(&kp1, &kp2) && sk == [0u8; 32] + } + QuickCheck::new().tests(10).quickcheck(prop as fn() -> _); + } + + #[test] + #[cfg(feature = "rand")] + fn sr25519_signature() { + let kp = Keypair::generate(); + let pk = kp.public(); + + let msg = "hello world".as_bytes(); + let sig = kp.sign(msg); + assert!(pk.verify(msg, &sig)); + + let mut invalid_sig = sig.clone(); + invalid_sig[3..6].copy_from_slice(&[10, 23, 42]); + assert!(!pk.verify(msg, &invalid_sig)); + + let invalid_msg = "h3ll0 w0rld".as_bytes(); + assert!(!pk.verify(invalid_msg, &sig)); + } +} diff --git a/interop-tests/Dockerfile.chromium b/interop-tests/Dockerfile.chromium index 73a9ab82ee7..ff9284f6da2 100644 --- a/interop-tests/Dockerfile.chromium +++ b/interop-tests/Dockerfile.chromium @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.5-labs FROM rust:1.83 as chef RUN rustup target add wasm32-unknown-unknown -RUN wget -q -O- https://github.com/rustwasm/wasm-pack/releases/download/v0.12.1/wasm-pack-v0.12.1-x86_64-unknown-linux-musl.tar.gz | tar -zx -C /usr/local/bin --strip-components 1 --wildcards "wasm-pack-*/wasm-pack" +RUN wget -q -O- https://github.com/drager/wasm-pack/releases/download/v0.12.1/wasm-pack-v0.12.1-x86_64-unknown-linux-musl.tar.gz | tar -zx -C /usr/local/bin --strip-components 1 --wildcards "wasm-pack-*/wasm-pack" RUN wget -q -O- https://github.com/WebAssembly/binaryen/releases/download/version_115/binaryen-version_115-x86_64-linux.tar.gz | tar -zx -C /usr/local/bin --strip-components 2 --wildcards "binaryen-version_*/bin/wasm-opt" RUN wget -q -O- https://github.com/LukeMathWalker/cargo-chef/releases/download/v0.1.62/cargo-chef-x86_64-unknown-linux-gnu.tar.gz | tar -zx -C /usr/local/bin WORKDIR /app diff --git a/libp2p/CHANGELOG.md b/libp2p/CHANGELOG.md index 2562a87da28..5c6c4a6865c 100644 --- a/libp2p/CHANGELOG.md +++ b/libp2p/CHANGELOG.md @@ -1,5 +1,12 @@ +## 0.56.1 + +- Fix `metrics` delegation to gossipsub protocol. + See [PR 6180](https://github.com/libp2p/rust-libp2p/pull/6180) + ## 0.56.0 +- Remove `async-std` support. + See [PR 6074](https://github.com/libp2p/rust-libp2p/pull/6074) - Remove deprecated `Transport::with_bandwidth_logging`, `SwarmBuilder::with_bandwidth_logging` and `TransportExt`. See [PR 5766](https://github.com/libp2p/rust-libp2p/pull/5766). @@ -13,8 +20,14 @@ - Make the `*-websys` variants (`libp2p-webrtc-websys`, `libp2p-websocket-websys`, `libp2p-webtransport-websys`) only available on wasm32 target architecture. See [PR 5891](https://github.com/libp2p/rust-libp2p/pull/5891). +- Remove TCP from the `async-std` swarm builder, as `async-std` support was removed from `libp2p-tcp` transport. + See [PR 5955](https://github.com/libp2p/rust-libp2p/pull/5955) + - Remove QUIC from the `async-std` swarm builder, as `async-std` support was removed from `libp2p-quic` transport. See [PR 5954](https://github.com/libp2p/rust-libp2p/pull/5954) + +- Remove DNS from the `async-std` swarm builder, as `async-std` support was removed from `libp2p-dns` transport. + See [PR 5959](https://github.com/libp2p/rust-libp2p/pull/5959) ## 0.55.0 diff --git a/libp2p/Cargo.toml b/libp2p/Cargo.toml index 0ff11d88787..587360b2336 100644 --- a/libp2p/Cargo.toml +++ b/libp2p/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p" edition.workspace = true rust-version = { workspace = true } description = "Peer-to-peer networking library" -version = "0.56.0" +version = "0.56.1" authors = ["Parity Technologies "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" @@ -12,7 +12,6 @@ categories = ["network-programming", "asynchronous"] [features] full = [ - "async-std", "autonat", "cbor", "dcutr", @@ -29,7 +28,6 @@ full = [ "memory-connection-limits", "metrics", "noise", - "peer-store", "ping", "plaintext", "pnet", @@ -53,13 +51,13 @@ full = [ "upnp", ] -async-std = ["libp2p-swarm/async-std", "libp2p-tcp?/async-io", "libp2p-dns?/async-std"] autonat = ["dep:libp2p-autonat"] cbor = ["libp2p-request-response?/cbor"] dcutr = ["dep:libp2p-dcutr", "libp2p-metrics?/dcutr"] dns = ["dep:libp2p-dns"] ecdsa = ["libp2p-identity/ecdsa"] ed25519 = ["libp2p-identity/ed25519"] +sr25519 = ["libp2p-identity/sr25519"] floodsub = ["dep:libp2p-floodsub"] gossipsub = ["dep:libp2p-gossipsub", "libp2p-metrics?/gossipsub"] identify = ["dep:libp2p-identify", "libp2p-metrics?/identify"] @@ -68,9 +66,8 @@ kad = ["dep:libp2p-kad", "libp2p-metrics?/kad"] macros = ["libp2p-swarm/macros"] mdns = ["dep:libp2p-mdns"] memory-connection-limits = ["dep:libp2p-memory-connection-limits"] -metrics = ["dep:libp2p-metrics"] +metrics = ["dep:libp2p-metrics", "libp2p-gossipsub?/metrics"] noise = ["dep:libp2p-noise"] -peer-store = ["dep:libp2p-peer-store"] ping = ["dep:libp2p-ping", "libp2p-metrics?/ping"] plaintext = ["dep:libp2p-plaintext"] pnet = ["dep:libp2p-pnet"] @@ -80,7 +77,7 @@ rendezvous = ["dep:libp2p-rendezvous"] request-response = ["dep:libp2p-request-response"] rsa = ["libp2p-identity/rsa"] secp256k1 = ["libp2p-identity/secp256k1"] -serde = ["libp2p-core/serde", "libp2p-kad?/serde", "libp2p-gossipsub?/serde"] +serde = ["libp2p-core/serde", "libp2p-kad?/serde", "libp2p-gossipsub?/serde", "libp2p-identity/serde"] tcp = ["dep:libp2p-tcp"] tls = ["dep:libp2p-tls"] tokio = ["libp2p-swarm/tokio", "libp2p-mdns?/tokio", "libp2p-tcp?/tokio", "libp2p-dns?/tokio", "libp2p-quic?/tokio", "libp2p-upnp?/tokio"] @@ -113,7 +110,6 @@ libp2p-identity = { workspace = true, features = ["rand"] } libp2p-kad = { workspace = true, optional = true } libp2p-metrics = { workspace = true, optional = true } libp2p-noise = { workspace = true, optional = true } -libp2p-peer-store = { workspace = true, optional = true } libp2p-ping = { workspace = true, optional = true } libp2p-plaintext = { workspace = true, optional = true } libp2p-pnet = { workspace = true, optional = true } @@ -123,6 +119,7 @@ libp2p-request-response = { workspace = true, optional = true } libp2p-swarm = { workspace = true } libp2p-yamux = { workspace = true, optional = true } multiaddr = { workspace = true } + pin-project = "1.0.0" thiserror = { workspace = true } @@ -143,7 +140,6 @@ libp2p-upnp = { workspace = true, optional = true } libp2p-websocket = { workspace = true, optional = true } [dev-dependencies] -async-std = { version = "1.6.2", features = ["attributes"] } tokio = { workspace = true, features = ["io-util", "io-std", "macros", "rt", "rt-multi-thread"] } libp2p-mplex = { workspace = true } diff --git a/libp2p/src/builder.rs b/libp2p/src/builder.rs index 4596fb99b50..95166bd34d4 100644 --- a/libp2p/src/builder.rs +++ b/libp2p/src/builder.rs @@ -102,28 +102,6 @@ mod tests { .build(); } - #[test] - #[cfg(all( - feature = "async-std", - feature = "tcp", - feature = "tls", - feature = "noise", - feature = "yamux", - ))] - fn async_std_tcp() { - let _ = SwarmBuilder::with_new_identity() - .with_async_std() - .with_tcp( - Default::default(), - libp2p_tls::Config::new, - libp2p_yamux::Config::default, - ) - .unwrap() - .with_behaviour(|_| libp2p_swarm::dummy::Behaviour) - .unwrap() - .build(); - } - #[test] #[cfg(all(feature = "tokio", feature = "quic"))] fn quic() { diff --git a/libp2p/src/builder/phase.rs b/libp2p/src/builder/phase.rs index 78ff49724c0..fa378273630 100644 --- a/libp2p/src/builder/phase.rs +++ b/libp2p/src/builder/phase.rs @@ -35,7 +35,7 @@ use super::{ select_muxer::SelectMuxerUpgrade, select_security::SelectSecurityUpgrade, SwarmBuilder, }; -#[allow(unreachable_pub)] +#[allow(unreachable_pub, dead_code)] pub trait IntoSecurityUpgrade { type Upgrade; type Error; @@ -77,7 +77,7 @@ where } } -#[allow(unreachable_pub)] +#[allow(unreachable_pub, dead_code)] pub trait IntoMultiplexerUpgrade { type Upgrade; diff --git a/libp2p/src/builder/phase/dns.rs b/libp2p/src/builder/phase/dns.rs index 83653836a34..7e6de8e8a6e 100644 --- a/libp2p/src/builder/phase/dns.rs +++ b/libp2p/src/builder/phase/dns.rs @@ -7,27 +7,6 @@ pub struct DnsPhase { pub(crate) transport: T, } -#[cfg(all(not(target_arch = "wasm32"), feature = "async-std", feature = "dns"))] -impl SwarmBuilder> { - pub fn with_dns( - self, - ) -> Result< - SwarmBuilder< - super::provider::AsyncStd, - WebsocketPhase, - >, - std::io::Error, - > { - Ok(SwarmBuilder { - keypair: self.keypair, - phantom: PhantomData, - phase: WebsocketPhase { - transport: libp2p_dns::async_std::Transport::system2(self.phase.transport)?, - }, - }) - } -} - #[cfg(all(not(target_arch = "wasm32"), feature = "tokio", feature = "dns"))] impl SwarmBuilder> { pub fn with_dns( @@ -49,30 +28,6 @@ impl SwarmBuilder SwarmBuilder> { - pub fn with_dns_config( - self, - cfg: libp2p_dns::ResolverConfig, - opts: libp2p_dns::ResolverOpts, - ) -> SwarmBuilder< - super::provider::AsyncStd, - WebsocketPhase, - > { - SwarmBuilder { - keypair: self.keypair, - phantom: PhantomData, - phase: WebsocketPhase { - transport: libp2p_dns::async_std::Transport::custom2( - self.phase.transport, - cfg, - opts, - ), - }, - } - } -} - #[cfg(all(not(target_arch = "wasm32"), feature = "tokio", feature = "dns"))] impl SwarmBuilder> { pub fn with_dns_config( diff --git a/libp2p/src/builder/phase/other_transport.rs b/libp2p/src/builder/phase/other_transport.rs index e77f0d62480..a7a8c6cc83d 100644 --- a/libp2p/src/builder/phase/other_transport.rs +++ b/libp2p/src/builder/phase/other_transport.rs @@ -66,22 +66,6 @@ impl } // Shortcuts -#[cfg(all(not(target_arch = "wasm32"), feature = "async-std", feature = "dns"))] -impl - SwarmBuilder> -{ - pub fn with_dns( - self, - ) -> Result< - SwarmBuilder< - super::provider::AsyncStd, - WebsocketPhase, - >, - std::io::Error, - > { - self.without_any_other_transports().with_dns() - } -} #[cfg(all(not(target_arch = "wasm32"), feature = "tokio", feature = "dns"))] impl SwarmBuilder> @@ -98,22 +82,6 @@ impl self.without_any_other_transports().with_dns() } } -#[cfg(all(not(target_arch = "wasm32"), feature = "async-std", feature = "dns"))] -impl - SwarmBuilder> -{ - pub fn with_dns_config( - self, - cfg: libp2p_dns::ResolverConfig, - opts: libp2p_dns::ResolverOpts, - ) -> SwarmBuilder< - super::provider::AsyncStd, - WebsocketPhase, - > { - self.without_any_other_transports() - .with_dns_config(cfg, opts) - } -} #[cfg(all(not(target_arch = "wasm32"), feature = "tokio", feature = "dns"))] impl SwarmBuilder> diff --git a/libp2p/src/builder/phase/provider.rs b/libp2p/src/builder/phase/provider.rs index 00a79e14a30..aa99d5518ce 100644 --- a/libp2p/src/builder/phase/provider.rs +++ b/libp2p/src/builder/phase/provider.rs @@ -11,10 +11,6 @@ pub enum NoProviderSpecified {} // Define enums for each of the possible runtime environments. These are used as markers in the // type-state pattern, allowing compile-time checks for the appropriate environment configuration. -#[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))] -/// Represents the AsyncStd runtime environment. -pub enum AsyncStd {} - #[cfg(all(not(target_arch = "wasm32"), feature = "tokio"))] /// Represents the Tokio runtime environment. pub enum Tokio {} @@ -27,18 +23,6 @@ pub enum WasmBindgen {} pub struct ProviderPhase {} impl SwarmBuilder { - /// Configures the SwarmBuilder to use the AsyncStd runtime. - /// This method is only available when compiling for non-Wasm - /// targets with the `async-std` feature enabled. - #[cfg(all(not(target_arch = "wasm32"), feature = "async-std"))] - pub fn with_async_std(self) -> SwarmBuilder { - SwarmBuilder { - keypair: self.keypair, - phantom: PhantomData, - phase: TcpPhase {}, - } - } - /// Configures the SwarmBuilder to use the Tokio runtime. /// This method is only available when compiling for non-Wasm /// targets with the `tokio` feature enabled diff --git a/libp2p/src/builder/phase/quic.rs b/libp2p/src/builder/phase/quic.rs index ebc0776ff62..b80a712dc07 100644 --- a/libp2p/src/builder/phase/quic.rs +++ b/libp2p/src/builder/phase/quic.rs @@ -149,22 +149,6 @@ impl SwarmBuilder SwarmBuilder> { - pub fn with_dns( - self, - ) -> Result< - SwarmBuilder< - super::provider::AsyncStd, - WebsocketPhase, - >, - std::io::Error, - > { - self.without_quic() - .without_any_other_transports() - .with_dns() - } -} #[cfg(all(not(target_arch = "wasm32"), feature = "tokio", feature = "dns"))] impl SwarmBuilder> { pub fn with_dns( @@ -181,21 +165,6 @@ impl SwarmBuilder SwarmBuilder> { - pub fn with_dns_config( - self, - cfg: libp2p_dns::ResolverConfig, - opts: libp2p_dns::ResolverOpts, - ) -> SwarmBuilder< - super::provider::AsyncStd, - WebsocketPhase, - > { - self.without_quic() - .without_any_other_transports() - .with_dns_config(cfg, opts) - } -} #[cfg(all(not(target_arch = "wasm32"), feature = "tokio", feature = "dns"))] impl SwarmBuilder> { pub fn with_dns_config( @@ -263,13 +232,7 @@ macro_rules! impl_quic_phase_with_websocket { } } } -impl_quic_phase_with_websocket!( - "async-std", - super::provider::AsyncStd, - rw_stream_sink::RwStreamSink< - libp2p_websocket::BytesConnection, - > -); + impl_quic_phase_with_websocket!( "tokio", super::provider::Tokio, diff --git a/libp2p/src/builder/phase/swarm.rs b/libp2p/src/builder/phase/swarm.rs index e751ad672e4..79b512a38af 100644 --- a/libp2p/src/builder/phase/swarm.rs +++ b/libp2p/src/builder/phase/swarm.rs @@ -42,13 +42,6 @@ macro_rules! impl_with_swarm_config { }; } -#[cfg(not(target_arch = "wasm32"))] -impl_with_swarm_config!( - "async-std", - super::provider::AsyncStd, - libp2p_swarm::Config::with_async_std_executor() -); - #[cfg(not(target_arch = "wasm32"))] impl_with_swarm_config!( "tokio", diff --git a/libp2p/src/builder/phase/tcp.rs b/libp2p/src/builder/phase/tcp.rs index 44c28d89888..f21ae109300 100644 --- a/libp2p/src/builder/phase/tcp.rs +++ b/libp2p/src/builder/phase/tcp.rs @@ -97,7 +97,6 @@ macro_rules! impl_tcp_builder { }; } -impl_tcp_builder!("async-std", super::provider::AsyncStd, async_io); impl_tcp_builder!("tokio", super::provider::Tokio, tokio); impl SwarmBuilder { @@ -215,13 +214,6 @@ macro_rules! impl_tcp_phase_with_websocket { } } } -impl_tcp_phase_with_websocket!( - "async-std", - super::provider::AsyncStd, - rw_stream_sink::RwStreamSink< - libp2p_websocket::BytesConnection, - > -); impl_tcp_phase_with_websocket!( "tokio", super::provider::Tokio, diff --git a/libp2p/src/builder/phase/websocket.rs b/libp2p/src/builder/phase/websocket.rs index 51ad0217069..369b101329a 100644 --- a/libp2p/src/builder/phase/websocket.rs +++ b/libp2p/src/builder/phase/websocket.rs @@ -115,16 +115,6 @@ macro_rules! impl_websocket_builder { }; } -impl_websocket_builder!( - "async-std", - super::provider::AsyncStd, - libp2p_dns::async_std::Transport::system(libp2p_tcp::async_io::Transport::new( - libp2p_tcp::Config::default(), - )), - rw_stream_sink::RwStreamSink< - libp2p_websocket::BytesConnection, - > -); impl_websocket_builder!( "tokio", super::provider::Tokio, diff --git a/libp2p/src/builder/select_muxer.rs b/libp2p/src/builder/select_muxer.rs index 93ae0547269..c60798a1ee1 100644 --- a/libp2p/src/builder/select_muxer.rs +++ b/libp2p/src/builder/select_muxer.rs @@ -34,6 +34,7 @@ use libp2p_core::{ pub struct SelectMuxerUpgrade(A, B); impl SelectMuxerUpgrade { + #[allow(dead_code)] pub fn new(a: A, b: B) -> Self { SelectMuxerUpgrade(a, b) } diff --git a/libp2p/src/builder/select_security.rs b/libp2p/src/builder/select_security.rs index 1ed760feb1b..e28083f73dc 100644 --- a/libp2p/src/builder/select_security.rs +++ b/libp2p/src/builder/select_security.rs @@ -42,6 +42,7 @@ impl SelectSecurityUpgrade { /// Combines two upgrades into an `SelectUpgrade`. /// /// The protocols supported by the first element have a higher priority. + #[allow(dead_code)] pub fn new(a: A, b: B) -> Self { SelectSecurityUpgrade(a, b) } diff --git a/libp2p/src/lib.rs b/libp2p/src/lib.rs index 1b02b7523b3..42461f8ef8e 100644 --- a/libp2p/src/lib.rs +++ b/libp2p/src/lib.rs @@ -81,9 +81,6 @@ pub use libp2p_metrics as metrics; #[cfg(feature = "noise")] #[doc(inline)] pub use libp2p_noise as noise; -#[cfg(feature = "peer-store")] -#[doc(inline)] -pub use libp2p_peer_store as peer_store; #[cfg(feature = "ping")] #[doc(inline)] pub use libp2p_ping as ping; diff --git a/misc/allow-block-list/CHANGELOG.md b/misc/allow-block-list/CHANGELOG.md index be7619269d0..c3d14568003 100644 --- a/misc/allow-block-list/CHANGELOG.md +++ b/misc/allow-block-list/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0 + + + ## 0.5.0 - Add getters & setters for the allowed/blocked peers. diff --git a/misc/allow-block-list/Cargo.toml b/misc/allow-block-list/Cargo.toml index 98512d0bf94..80f6fbb218b 100644 --- a/misc/allow-block-list/Cargo.toml +++ b/misc/allow-block-list/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-allow-block-list" edition.workspace = true rust-version = { workspace = true } description = "Allow/block list connection management for libp2p." -version = "0.5.0" +version = "0.6.0" license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" keywords = ["peer-to-peer", "libp2p", "networking"] diff --git a/misc/connection-limits/CHANGELOG.md b/misc/connection-limits/CHANGELOG.md index 52e3bcdccb4..5f5f7cb41ee 100644 --- a/misc/connection-limits/CHANGELOG.md +++ b/misc/connection-limits/CHANGELOG.md @@ -1,9 +1,11 @@ -## 0.5.1 +## 0.6.0 - Allow setting Peer IDs for bypassing limit check. Connections to the specified peers won't be counted toward limits. See [PR 5720](https://github.com/libp2p/rust-libp2p/pull/5720). + + ## 0.5.0 - Deprecate `void` crate. diff --git a/misc/connection-limits/Cargo.toml b/misc/connection-limits/Cargo.toml index 56f3ab091d2..4e72de4a20c 100644 --- a/misc/connection-limits/Cargo.toml +++ b/misc/connection-limits/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-connection-limits" edition.workspace = true rust-version = { workspace = true } description = "Connection limits for libp2p." -version = "0.5.1" +version = "0.6.0" license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" keywords = ["peer-to-peer", "libp2p", "networking"] diff --git a/misc/memory-connection-limits/CHANGELOG.md b/misc/memory-connection-limits/CHANGELOG.md index ed16fa37eb3..f854edba6a4 100644 --- a/misc/memory-connection-limits/CHANGELOG.md +++ b/misc/memory-connection-limits/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.0 + + + ## 0.4.0 - Deprecate `void` crate. diff --git a/misc/memory-connection-limits/Cargo.toml b/misc/memory-connection-limits/Cargo.toml index dbbaafaf0de..da4df0c5244 100644 --- a/misc/memory-connection-limits/Cargo.toml +++ b/misc/memory-connection-limits/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-memory-connection-limits" edition.workspace = true rust-version = { workspace = true } description = "Memory usage based connection limits for libp2p." -version = "0.4.0" +version = "0.5.0" license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" keywords = ["peer-to-peer", "libp2p", "networking"] diff --git a/misc/memory-connection-limits/src/lib.rs b/misc/memory-connection-limits/src/lib.rs index 7905e39ac18..b19a1573fba 100644 --- a/misc/memory-connection-limits/src/lib.rs +++ b/misc/memory-connection-limits/src/lib.rs @@ -82,7 +82,7 @@ const MAX_STALE_DURATION: Duration = Duration::from_millis(100); impl Behaviour { /// Sets the process memory usage threshold in absolute bytes. /// - /// New inbound and outbound connections will be denied when the threshold is reached. + /// New inbound and outbound connections will be denied when the threshold is exceeded. pub fn with_max_bytes(max_allowed_bytes: usize) -> Self { Self { max_allowed_bytes, @@ -95,7 +95,7 @@ impl Behaviour { /// Sets the process memory usage threshold in the percentage of the total physical memory. /// - /// New inbound and outbound connections will be denied when the threshold is reached. + /// New inbound and outbound connections will be denied when the threshold is exceeded. pub fn with_max_percentage(percentage: f64) -> Self { use sysinfo::{RefreshKind, System}; diff --git a/misc/memory-connection-limits/tests/max_bytes.rs b/misc/memory-connection-limits/tests/max_bytes.rs index 442e38bdf1b..16ff8ed3ea0 100644 --- a/misc/memory-connection-limits/tests/max_bytes.rs +++ b/misc/memory-connection-limits/tests/max_bytes.rs @@ -31,8 +31,11 @@ use util::*; #[tokio::test] async fn max_bytes() { + // These tests use connections as unit to test the memory limit. + // Each connection consumes approximately 1MB of memory, so we give + // one connection as buffer for test stability (CONNECTION_LIMIT - 1) on line 35. const CONNECTION_LIMIT: usize = 20; - let max_allowed_bytes = CONNECTION_LIMIT * 1024 * 1024; + let max_allowed_bytes = (CONNECTION_LIMIT - 1) * 1024 * 1024; let mut network = Swarm::new_ephemeral_tokio(|_| TestBehaviour { connection_limits: Behaviour::with_max_bytes(max_allowed_bytes), diff --git a/misc/memory-connection-limits/tests/max_percentage.rs b/misc/memory-connection-limits/tests/max_percentage.rs index cddb29d6964..de1585eb279 100644 --- a/misc/memory-connection-limits/tests/max_percentage.rs +++ b/misc/memory-connection-limits/tests/max_percentage.rs @@ -59,7 +59,10 @@ async fn max_percentage() { // Adds current mem usage to the limit and update let current_mem = memory_stats::memory_stats().unwrap().physical_mem; - let max_allowed_bytes = current_mem + CONNECTION_LIMIT * 1024 * 1024; + // These tests use connections as unit to test the memory limit. + // Each connection consumes approximately 1MB of memory, so we give + // one connection as buffer for test stability (CONNECTION_LIMIT - 1) on line 35. + let max_allowed_bytes = current_mem + (CONNECTION_LIMIT - 1) * 1024 * 1024; network.behaviour_mut().connection_limits = Behaviour::with_max_percentage( max_allowed_bytes as f64 / system_info.total_memory() as f64, ); diff --git a/misc/metrics/CHANGELOG.md b/misc/metrics/CHANGELOG.md index 7402b996267..bab690c4d98 100644 --- a/misc/metrics/CHANGELOG.md +++ b/misc/metrics/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.17.1 + +- Fix panic in swarm metrics when `ConnectionClosed` events are received for connections that were established before metrics collection started. + See [PR 6158](https://github.com/libp2p/rust-libp2p/pull/6158). + ## 0.17.0 - Update `prometheus-client` to `0.23.0`. @@ -6,6 +11,8 @@ - Add `ReservationClosed` as a relay metric. See [PR 5869](https://github.com/libp2p/rust-libp2p/pull/5869). + + ## 0.16.0 diff --git a/misc/metrics/Cargo.toml b/misc/metrics/Cargo.toml index 591a904aee3..32be1e8fdac 100644 --- a/misc/metrics/Cargo.toml +++ b/misc/metrics/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-metrics" edition.workspace = true rust-version = { workspace = true } description = "Metrics for libp2p" -version = "0.17.0" +version = "0.17.1" authors = ["Max Inden "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" diff --git a/misc/metrics/src/kad.rs b/misc/metrics/src/kad.rs index 0a2a8038511..1b4d68f31ca 100644 --- a/misc/metrics/src/kad.rs +++ b/misc/metrics/src/kad.rs @@ -318,7 +318,6 @@ struct GetRecordResult { #[derive(EncodeLabelValue, Hash, Clone, Eq, PartialEq, Debug)] enum GetRecordError { NotFound, - QuorumFailed, Timeout, } @@ -328,9 +327,6 @@ impl From<&libp2p_kad::GetRecordError> for GetRecordResult { libp2p_kad::GetRecordError::NotFound { .. } => GetRecordResult { error: GetRecordError::NotFound, }, - libp2p_kad::GetRecordError::QuorumFailed { .. } => GetRecordResult { - error: GetRecordError::QuorumFailed, - }, libp2p_kad::GetRecordError::Timeout { .. } => GetRecordResult { error: GetRecordError::Timeout, }, diff --git a/misc/metrics/src/swarm.rs b/misc/metrics/src/swarm.rs index 6e95d082de6..abd76691979 100644 --- a/misc/metrics/src/swarm.rs +++ b/misc/metrics/src/swarm.rs @@ -228,15 +228,20 @@ impl super::Recorder> for Metrics { }, cause: cause.as_ref().map(Into::into), }; - self.connections_duration.get_or_create(&labels).observe( - self.connections - .lock() - .expect("lock not to be poisoned") - .remove(connection_id) - .expect("closed connection to previously be established") - .elapsed() - .as_secs_f64(), - ); + + // Only record connection duration if we have a record of when it was established. + // This gracefully handles cases where ConnectionClosed events are received + // for connections that were established before metrics collection started. + if let Some(established_time) = self + .connections + .lock() + .expect("lock not to be poisoned") + .remove(connection_id) + { + self.connections_duration + .get_or_create(&labels) + .observe(established_time.elapsed().as_secs_f64()); + } } SwarmEvent::IncomingConnection { send_back_addr, .. } => { self.connections_incoming @@ -453,3 +458,90 @@ impl From<&libp2p_swarm::ListenError> for IncomingConnectionError { } } } + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use libp2p_core::ConnectedPoint; + use libp2p_swarm::{ConnectionId, SwarmEvent}; + use prometheus_client::registry::Registry; + + use super::*; + use crate::Recorder; + + #[test] + fn test_connection_closed_without_established() { + let mut registry = Registry::default(); + let metrics = Metrics::new(&mut registry); + + // Create a fake ConnectionClosed event for a connection that was never tracked. + let connection_id = ConnectionId::new_unchecked(1); + let endpoint = ConnectedPoint::Dialer { + address: "/ip4/127.0.0.1/tcp/8080".parse().unwrap(), + role_override: libp2p_core::Endpoint::Dialer, + port_use: libp2p_core::transport::PortUse::New, + }; + + let event = SwarmEvent::<()>::ConnectionClosed { + peer_id: libp2p_identity::PeerId::random(), + connection_id, + endpoint, + num_established: 0, + cause: None, + }; + + // This should NOT panic. + metrics.record(&event); + + // Verify that the connections map is still empty (no connection was removed). + let connections = metrics.connections.lock().expect("lock not to be poisoned"); + assert!(connections.is_empty()); + } + + #[test] + fn test_connection_established_then_closed() { + let mut registry = Registry::default(); + let metrics = Metrics::new(&mut registry); + + let connection_id = ConnectionId::new_unchecked(1); + let endpoint = ConnectedPoint::Dialer { + address: "/ip4/127.0.0.1/tcp/8080".parse().unwrap(), + role_override: libp2p_core::Endpoint::Dialer, + port_use: libp2p_core::transport::PortUse::New, + }; + + // First, establish a connection. + let established_event = SwarmEvent::<()>::ConnectionEstablished { + peer_id: libp2p_identity::PeerId::random(), + connection_id, + endpoint: endpoint.clone(), + num_established: std::num::NonZeroU32::new(1).unwrap(), + concurrent_dial_errors: None, + established_in: Duration::from_millis(100), + }; + + metrics.record(&established_event); + + // Verify connection was added. + { + let connections = metrics.connections.lock().expect("lock not to be poisoned"); + assert!(connections.contains_key(&connection_id)); + } + + // Now close the connection. + let closed_event = SwarmEvent::<()>::ConnectionClosed { + peer_id: libp2p_identity::PeerId::random(), + connection_id, + endpoint, + num_established: 0, + cause: None, + }; + + metrics.record(&closed_event); + + // Verify connection was removed. + let connections = metrics.connections.lock().expect("lock not to be poisoned"); + assert!(!connections.contains_key(&connection_id)); + } +} diff --git a/misc/multistream-select/Cargo.toml b/misc/multistream-select/Cargo.toml index 1b00218c514..2c2435e4566 100644 --- a/misc/multistream-select/Cargo.toml +++ b/misc/multistream-select/Cargo.toml @@ -19,11 +19,12 @@ smallvec = "1.13.2" unsigned-varint = { workspace = true } [dev-dependencies] -async-std = { version = "1.6.2", features = ["attributes"] } futures_ringbuf = "0.4.0" quickcheck = { workspace = true } rw-stream-sink = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } +tokio = { workspace = true, features = ["full"] } +tokio-util = { version = "0.7", features = ["compat"] } # Passing arguments to the docsrs builder in order to properly document cfg's. # More information: https://docs.rs/about/builds#cross-compiling diff --git a/misc/multistream-select/src/dialer_select.rs b/misc/multistream-select/src/dialer_select.rs index 1d13e94910d..2c501a4f2f8 100644 --- a/misc/multistream-select/src/dialer_select.rs +++ b/misc/multistream-select/src/dialer_select.rs @@ -208,13 +208,16 @@ where #[cfg(test)] mod tests { + use std::time::Duration; - use async_std::{ - future::timeout, + use quickcheck::{Arbitrary, Gen, GenRange}; + use tokio::{ net::{TcpListener, TcpStream}, + runtime::Runtime, + time::timeout, }; - use quickcheck::{Arbitrary, Gen, GenRange}; + use tokio_util::compat::TokioAsyncReadCompatExt; use tracing::metadata::LevelFilter; use tracing_subscriber::EnvFilter; @@ -226,7 +229,7 @@ mod tests { async fn run(version: Version) { let (client_connection, server_connection) = futures_ringbuf::Endpoint::pair(100, 100); - let server = async_std::task::spawn(async move { + let server = tokio::task::spawn(async move { let protos = vec!["/proto1", "/proto2"]; let (proto, mut io) = listener_select_proto(server_connection, protos) .await @@ -242,7 +245,7 @@ mod tests { io.flush().await.unwrap(); }); - let client = async_std::task::spawn(async move { + let client = tokio::task::spawn(async move { let protos = vec!["/proto3", "/proto2"]; let (proto, mut io) = dialer_select_proto(client_connection, protos, version) .await @@ -258,12 +261,13 @@ mod tests { assert_eq!(out, b"pong"); }); - server.await; - client.await; + server.await.unwrap(); + client.await.unwrap(); } - async_std::task::block_on(run(Version::V1)); - async_std::task::block_on(run(Version::V1Lazy)); + let rt = Runtime::new().unwrap(); + rt.block_on(run(Version::V1)); + rt.block_on(run(Version::V1Lazy)); } /// Tests the expected behaviour of failed negotiations. @@ -283,12 +287,13 @@ mod tests { ) .try_init(); - async_std::task::block_on(async move { + let rt = Runtime::new().unwrap(); + rt.block_on(async move { let listener = TcpListener::bind("0.0.0.0:0").await.unwrap(); let addr = listener.local_addr().unwrap(); - let server = async_std::task::spawn(async move { - let server_connection = listener.accept().await.unwrap().0; + let server = tokio::task::spawn(async move { + let server_connection = listener.accept().await.unwrap().0.compat(); let io = match timeout( Duration::from_secs(2), @@ -309,8 +314,8 @@ mod tests { } }); - let client = async_std::task::spawn(async move { - let client_connection = TcpStream::connect(addr).await.unwrap(); + let client = tokio::task::spawn(async move { + let client_connection = TcpStream::connect(addr).await.unwrap().compat(); let mut io = match timeout( Duration::from_secs(2), @@ -336,8 +341,8 @@ mod tests { } }); - server.await; - client.await; + server.await.unwrap(); + client.await.unwrap(); tracing::info!("---------------------------------------") }); @@ -348,12 +353,12 @@ mod tests { .quickcheck(prop as fn(_, _, _, _)); } - #[async_std::test] + #[tokio::test] async fn v1_lazy_do_not_wait_for_negotiation_on_poll_close() { let (client_connection, _server_connection) = futures_ringbuf::Endpoint::pair(1024 * 1024, 1); - let client = async_std::task::spawn(async move { + let client = tokio::task::spawn(async move { // Single protocol to allow for lazy (or optimistic) protocol negotiation. let protos = vec!["/proto1"]; let (proto, mut io) = dialer_select_proto(client_connection, protos, Version::V1Lazy) @@ -366,9 +371,12 @@ mod tests { io.close().await.unwrap(); }); - async_std::future::timeout(Duration::from_secs(10), client) - .await - .unwrap(); + match tokio::time::timeout(Duration::from_secs(10), client).await { + Ok(join_result) => join_result.expect("Client task should complete successfully"), + Err(_elapsed) => { + panic!("Expected the client task to complete before timeout"); + } + } } #[derive(Clone, Debug)] diff --git a/misc/multistream-select/src/length_delimited.rs b/misc/multistream-select/src/length_delimited.rs index 8062455de46..e9d76331796 100644 --- a/misc/multistream-select/src/length_delimited.rs +++ b/misc/multistream-select/src/length_delimited.rs @@ -388,6 +388,7 @@ mod tests { use futures::{io::Cursor, prelude::*}; use quickcheck::*; + use tokio::runtime::Runtime; use crate::length_delimited::LengthDelimited; @@ -491,9 +492,10 @@ mod tests { fn prop(frames: Vec>) -> TestResult { let (client_connection, server_connection) = futures_ringbuf::Endpoint::pair(100, 100); - async_std::task::block_on(async move { + let rt = Runtime::new().unwrap(); + rt.block_on(async move { let expected_frames = frames.clone(); - let server = async_std::task::spawn(async move { + let server = tokio::task::spawn(async move { let mut connec = rw_stream_sink::RwStreamSink::new(LengthDelimited::new(server_connection)); @@ -510,15 +512,15 @@ mod tests { } }); - let client = async_std::task::spawn(async move { + let client = tokio::task::spawn(async move { let mut connec = LengthDelimited::new(client_connection); for frame in frames { connec.send(From::from(frame)).await.unwrap(); } }); - server.await; - client.await; + server.await.unwrap(); + client.await.unwrap(); }); TestResult::passed() diff --git a/misc/multistream-select/src/lib.rs b/misc/multistream-select/src/lib.rs index 96432de6cb0..725c3a0e2ba 100644 --- a/misc/multistream-select/src/lib.rs +++ b/misc/multistream-select/src/lib.rs @@ -69,17 +69,19 @@ //! For a dialer: //! //! ```no_run -//! use async_std::net::TcpStream; //! use futures::prelude::*; //! use multistream_select::{dialer_select_proto, Version}; +//! use tokio::{net::TcpStream, runtime::Runtime}; +//! use tokio_util::compat::TokioAsyncReadCompatExt; //! -//! async_std::task::block_on(async move { +//! let rt = Runtime::new().unwrap(); +//! rt.block_on(async move { //! let socket = TcpStream::connect("127.0.0.1:10333").await.unwrap(); +//! let compat_socket = socket.compat(); //! //! let protos = vec!["/echo/1.0.0", "/echo/2.5.0"]; -//! let (protocol, _io) = dialer_select_proto(socket, protos, Version::V1) -//! .await -//! .unwrap(); +//! let result = dialer_select_proto(compat_socket, protos, Version::V1).await; +//! let (protocol, _io) = result.unwrap(); //! //! println!("Negotiated protocol: {:?}", protocol); //! // You can now use `_io` to communicate with the remote. @@ -116,7 +118,7 @@ pub enum Version { /// /// This strategy is only applicable for the node with the role of "dialer" /// in the negotiation and only if the dialer supports just a single - /// application protocol. In that case the dialer immedidately "settles" + /// application protocol. In that case the dialer immediately "settles" /// on that protocol, buffering the negotiation messages to be sent /// with the first round of application protocol data (or an attempt /// is made to read from the `Negotiated` I/O stream). diff --git a/misc/peer-store/Cargo.toml b/misc/peer-store/Cargo.toml index 5f6bf598cbf..490facb13fb 100644 --- a/misc/peer-store/Cargo.toml +++ b/misc/peer-store/Cargo.toml @@ -9,9 +9,9 @@ publish = false rust-version.workspace = true [dependencies] +hashlink = { workspace = true } libp2p-core = { workspace = true } libp2p-swarm = { workspace = true } -lru = "0.12.3" libp2p-identity = { workspace = true, optional = true } [dev-dependencies] diff --git a/misc/peer-store/src/memory_store.rs b/misc/peer-store/src/memory_store.rs index 8fbe31c6292..ed996dc6fdd 100644 --- a/misc/peer-store/src/memory_store.rs +++ b/misc/peer-store/src/memory_store.rs @@ -9,14 +9,14 @@ //! ``` use std::{ - collections::{HashMap, VecDeque}, + collections::VecDeque, num::NonZeroUsize, task::{Poll, Waker}, }; +use hashlink::LruCache; use libp2p_core::{Multiaddr, PeerId}; use libp2p_swarm::{behaviour::ConnectionEstablished, DialError, FromSwarm}; -use lru::LruCache; use super::Store; @@ -49,10 +49,9 @@ pub enum Event { /// A in-memory store that uses LRU cache for bounded storage of addresses /// and a frequency-based ordering of addresses. -#[derive(Default)] pub struct MemoryStore { /// The internal store. - records: HashMap>, + records: LruCache>, /// Events to emit to [`Behaviour`](crate::Behaviour) and [`Swarm`](libp2p_swarm::Swarm). pending_events: VecDeque, /// Config of the store. @@ -65,8 +64,8 @@ impl MemoryStore { /// Create a new [`MemoryStore`] with the given config. pub fn new(config: Config) -> Self { Self { + records: LruCache::new(config.peer_capacity().get()), config, - records: HashMap::new(), pending_events: VecDeque::default(), waker: None, } @@ -96,7 +95,7 @@ impl MemoryStore { let record = self .records .entry(*peer) - .or_insert(PeerRecord::new(self.config.record_capacity)); + .or_insert_with(|| PeerRecord::new(self.config.record_capacity)); let is_new = record.add_address(address, is_permanent); if is_new { self.push_event_and_wake(Event::PeerAddressAdded { @@ -137,7 +136,7 @@ impl MemoryStore { /// Get a reference to a peer's custom data. pub fn get_custom_data(&self, peer: &PeerId) -> Option<&T> { - self.records.get(peer).and_then(|r| r.get_custom_data()) + self.records.peek(peer).and_then(|r| r.get_custom_data()) } /// Take ownership of the internal data, leaving `None` in its place. @@ -240,7 +239,7 @@ impl Store for MemoryStore { } fn addresses_of_peer(&self, peer: &PeerId) -> Option> { - self.records.get(peer).map(|record| record.addresses()) + self.records.peek(peer).map(|record| record.addresses()) } fn poll(&mut self, cx: &mut std::task::Context<'_>) -> Poll { @@ -257,6 +256,7 @@ impl Store for MemoryStore { /// Config for [`MemoryStore`]. The available options are documented via their setters. #[derive(Debug, Clone)] pub struct Config { + peer_capacity: NonZeroUsize, record_capacity: NonZeroUsize, remove_addr_on_dial_error: bool, } @@ -264,6 +264,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { + peer_capacity: NonZeroUsize::try_from(1000).expect("1000 > 0"), record_capacity: NonZeroUsize::try_from(8).expect("8 > 0"), remove_addr_on_dial_error: true, } @@ -271,12 +272,24 @@ impl Default for Config { } impl Config { + pub fn peer_capacity(&self) -> &NonZeroUsize { + &self.peer_capacity + } + /// The capacity of the address store per peer. + /// + /// The least recently updated peer will be discarded to make room for a new peer. + /// + /// `1000` by default. + pub fn set_peer_capacity(mut self, capacity: NonZeroUsize) -> Self { + self.peer_capacity = capacity; + self + } pub fn record_capacity(&self) -> &NonZeroUsize { &self.record_capacity } - /// The capacity of an address store. + /// The capacity of the address store per peer. /// - /// The least active address will be discarded to make room for new address. + /// The least active address will be discarded to make room for a new address. /// /// `8` by default. pub fn set_record_capacity(mut self, capacity: NonZeroUsize) -> Self { @@ -317,7 +330,7 @@ pub struct PeerRecord { impl PeerRecord { pub(crate) fn new(cap: NonZeroUsize) -> Self { Self { - addresses: LruCache::new(cap), + addresses: LruCache::new(cap.get()), custom_data: None, } } @@ -325,7 +338,7 @@ impl PeerRecord { /// Iterate over all addresses. More recently-used address comes first. /// Does not change the order. pub fn addresses(&self) -> impl Iterator { - self.addresses.iter().map(|(addr, _)| addr) + self.addresses.iter().rev().map(|(addr, _)| addr) } /// Update the address in the LRU cache, promote it to the front if it exists, @@ -335,14 +348,13 @@ impl PeerRecord { pub fn add_address(&mut self, address: &Multiaddr, is_permanent: bool) -> bool { if let Some(was_permanent) = self.addresses.get(address) { if !*was_permanent && is_permanent { - self.addresses - .get_or_insert(address.clone(), || is_permanent); + self.addresses.insert(address.clone(), is_permanent); } - return false; + false + } else { + self.addresses.insert(address.clone(), is_permanent); + true } - self.addresses - .get_or_insert(address.clone(), || is_permanent); - true } /// Remove the address in the LRU cache regardless of its position. @@ -353,7 +365,7 @@ impl PeerRecord { if !force && self.addresses.peek(address) == Some(&true) { return false; } - self.addresses.pop(address).is_some() + self.addresses.remove(address).is_some() } pub fn get_custom_data(&self) -> Option<&T> { @@ -399,14 +411,14 @@ mod test { store.add_address(&peer, &addr1); store.add_address(&peer, &addr2); store.add_address(&peer, &addr3); - assert!( + assert_eq!( store .records .get(&peer) .expect("peer to be in the store") .addresses() - .collect::>() - == vec![&addr3, &addr2, &addr1] + .collect::>(), + vec![&addr3, &addr2, &addr1] ); store.add_address(&peer, &addr1); assert!( diff --git a/multiaddr/.gitignore b/multiaddr/.gitignore new file mode 100644 index 00000000000..5057d9ef273 --- /dev/null +++ b/multiaddr/.gitignore @@ -0,0 +1,3 @@ +target +Cargo.lock +*.rs.bk diff --git a/multiaddr/CHANGELOG.md b/multiaddr/CHANGELOG.md new file mode 100644 index 00000000000..349e539c9e3 --- /dev/null +++ b/multiaddr/CHANGELOG.md @@ -0,0 +1,183 @@ +# 0.18.3 + +- Add `starts_with` on `Multiaddr`. See [PR 119]. + +[PR 119]: https://github.com/multiformats/rust-multiaddr/pull/119 + +# 0.18.2 + +- Implement missing protocols. See [PR 110]. + [PR 110]: https://github.com/multiformats/rust-multiaddr/pull/110. + +- Re-export `libp2p_identity::PeerId`. See [PR 108]. + [PR 108]: https://github.com/multiformats/rust-multiaddr/pull/108. + +- Avoid allocations in Display and Debug of Multiaddr. See [PR 106]. + [PR 106]: https://github.com/multiformats/rust-multiaddr/pull/106 + +# 0.18.1 + +- Add `with_p2p` on `Multiaddr`. See [PR 102]. + +[PR 102]: https://github.com/multiformats/rust-multiaddr/pull/102 + +# 0.18.0 + +- Add `WebTransport` instance for `Multiaddr`. See [PR 70]. + +- Disable all features of `multihash`. See [PR 77]. + +- Mark `Protocol` as `#[non_exhaustive]`. See [PR 82]. + +- Rename `Protocol::WebRTC` to `Protocol::WebRTCDirect`. + See [multiformats/multiaddr discussion] for context. + Remove deprecated support for `/webrtc` in favor of the existing `/webrtc-direct` string representation. + **Note that this is a breaking change.** + +- Make `/p2p` typesafe, i.e. have `Protocol::P2p` contain a `PeerId` instead of a `Multihash`. + See [PR 83]. + +[multiformats/multiaddr discussion]: https://github.com/multiformats/multiaddr/pull/150#issuecomment-1468791586 +[PR 70]: https://github.com/multiformats/rust-multiaddr/pull/70 +[PR 77]: https://github.com/multiformats/rust-multiaddr/pull/77 +[PR 82]: https://github.com/multiformats/rust-multiaddr/pull/82 +[PR 83]: https://github.com/multiformats/rust-multiaddr/pull/83 + +# 0.17.1 + +- Rename string representation of `WebRTC` protocol from `/webrtc` to `/webrt-direct`. + For backwards compatibility `/webrtc` will still be decoded to `Protocol::WebRTC`, but `Protocol::WebRTC` will from now on always be encoded as `/webrtc-direct`. + See [multiformats/multiaddr discussion] and [PR 84] for context. + ``` rust + assert_eq!( + Multiaddr::empty().with(Protocol::WebRTC), + "/webrtc".parse().unwrap(), + ); + assert_eq!( + Multiaddr::empty().with(Protocol::WebRTC), + "/webrtc-direct".parse().unwrap(), + ); + assert_eq!( + "/webrtc-direct", + Multiaddr::empty().with(Protocol::WebRTC).to_string(), + ); + assert_ne!( + "/webrtc", + Multiaddr::empty().with(Protocol::WebRTC).to_string(), + ); + ``` + +[PR 84]: https://github.com/multiformats/rust-multiaddr/pull/84 + +# 0.17.0 + +- Update to multihash `v0.17`. See [PR 63]. + +[PR 63]: https://github.com/multiformats/rust-multiaddr/pull/63 + +# 0.16.0 [2022-11-04] + +- Create `protocol_stack` for Multiaddr. See [PR 60]. + +- Add `QuicV1` instance for `Multiaddr`. See [PR 64]. + +[PR 60]: https://github.com/multiformats/rust-multiaddr/pull/60 +[PR 64]: https://github.com/multiformats/rust-multiaddr/pull/64 + +# 0.15.0 [2022-10-20] + +- Add `WebRTC` instance for `Multiaddr`. See [PR 59]. +- Add `Certhash` instance for `Multiaddr`. See [PR 59]. + +- Add support for Noise protocol. See [PR 53]. + +- Use `multibase` instead of `bs58` for base58 encoding. See [PR 56]. + +[PR 53]: https://github.com/multiformats/rust-multiaddr/pull/53 +[PR 56]: https://github.com/multiformats/rust-multiaddr/pull/56 +[PR 59]: https://github.com/multiformats/rust-multiaddr/pull/59 + +# 0.14.0 [2022-02-02] + +- Add support for TLS protocol (see [PR 48]). + +- Update to `multihash` `v0.15` (see [PR 50]). + +- Update to `multihash` `v0.16` (see [PR 51]). + +[PR 48]: https://github.com/multiformats/rust-multiaddr/pull/48 +[PR 50]: https://github.com/multiformats/rust-multiaddr/pull/50 +[PR 50]: https://github.com/multiformats/rust-multiaddr/pull/51 + +# 0.13.0 [2021-07-08] + +- Update to multihash v0.14.0 (see [PR 44]). + +- Update to rand v0.8.4 (see [PR 45]). + +[PR 44]: https://github.com/multiformats/rust-multiaddr/pull/44 +[PR 45]: https://github.com/multiformats/rust-multiaddr/pull/45 + +# 0.12.0 [2021-05-26] + +- Merge [multiaddr] and [parity-multiaddr] (see [PR 40]). + + - Functionality to go from a `u64` to a `multiaddr::Protocol` and back is + removed. Please open an issue on [multiaddr] in case this is still needed. + + - Given that `multiaddr::Protocol` now represents both the protocol + identifier as well as the protocol data (e.g. protocol identifier `55` + (`dns6`) and protocol data `some-domain.example`) `multiaddr::Protocol` is + no longer `Copy`. + +[multiaddr]: https://github.com/multiformats/rust-multiaddr +[parity-multiaddr]: https://github.com/libp2p/rust-libp2p/blob/master/misc/multiaddr/ +[PR 40]: https://github.com/multiformats/rust-multiaddr/pull/40 + +# 0.11.2 [2021-03-17] + +- Add `Multiaddr::ends_with()`. + +# 0.11.1 [2021-02-15] + +- Update dependencies + +# 0.11.0 [2021-01-12] + +- Update dependencies + +# 0.10.1 [2021-01-12] + +- Fix compilation with serde-1.0.119. + [PR 1912](https://github.com/libp2p/rust-libp2p/pull/1912) + +# 0.10.0 [2020-11-25] + +- Upgrade multihash to `0.13`. + +# 0.9.6 [2020-11-17] + +- Move the `from_url` module and functionality behind the `url` feature, + enabled by default. + [PR 1843](https://github.com/libp2p/rust-libp2p/pull/1843). + +# 0.9.5 [2020-11-14] + +- Limit initial memory allocation in `visit_seq`. + [PR 1833](https://github.com/libp2p/rust-libp2p/pull/1833). + +# 0.9.4 [2020-11-09] + +- Update dependencies. + +# 0.9.3 [2020-10-16] + +- Update dependencies. + +# 0.9.2 [2020-08-31] + +- Add `Ord` instance for `Multiaddr`. + +# 0.9.1 [2020-06-22] + +- Updated dependencies. diff --git a/multiaddr/Cargo.toml b/multiaddr/Cargo.toml new file mode 100644 index 00000000000..1c257b39b00 --- /dev/null +++ b/multiaddr/Cargo.toml @@ -0,0 +1,42 @@ +[package] +authors = ["dignifiedquire ", "Parity Technologies "] +description = "Implementation of the multiaddr format" +edition = "2021" +rust-version = "1.59.0" +repository = "https://github.com/multiformats/rust-multiaddr" +keywords = ["multiaddr", "ipfs"] +license = "MIT" +name = "multiaddr" +readme = "README.md" +version = "0.18.3" + +[features] +default = ["url"] + +[dependencies] +arrayref = "0.3" +byteorder = "1.5.0" +bytes = "1.7.2" +data-encoding = "2.6.0" +multibase = "0.9.1" +multihash = "0.19" +percent-encoding = "2.3.1" +serde = "1.0.209" +static_assertions = "1.1" +unsigned-varint = "0.8" +url = { version = "2.5.0", optional = true, default-features = false } +libp2p-identity = { workspace = true, version = "0.2.13", features = ["peerid"] } + +[dev-dependencies] +bincode = "1" +quickcheck = { version = "1.0.3", default-features = false } +rand = "0.9.0" +serde_json = "1.0" + +# Passing arguments to the docsrs builder in order to properly document cfg's. +# More information: https://docs.rs/about/builds#cross-compiling +[package.metadata.docs.rs] +all-features = true + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(nightly)'] } diff --git a/multiaddr/LICENSE b/multiaddr/LICENSE new file mode 100644 index 00000000000..233fd7bd7d5 --- /dev/null +++ b/multiaddr/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (C) 2015-2016 Friedel Ziegelmayer + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Status API Training Shop Blog About Pricing diff --git a/multiaddr/README.md b/multiaddr/README.md new file mode 100644 index 00000000000..f4766a24323 --- /dev/null +++ b/multiaddr/README.md @@ -0,0 +1,63 @@ +# rust-multiaddr + +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) +[![](https://img.shields.io/badge/project-multiformats-blue.svg?style=flat-square)](https://github.com/multiformats/multiformats) +[![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](https://webchat.freenode.net/?channels=%23ipfs) +[![Travis CI](https://img.shields.io/travis/multiformats/rust-multiaddr.svg?style=flat-square&branch=master)](https://travis-ci.org/multiformats/rust-multiaddr) +[![codecov.io](https://img.shields.io/codecov/c/github/multiformats/rust-multiaddr.svg?style=flat-square&branch=master)](https://codecov.io/github/multiformats/rust-multiaddr?branch=master) +[![](https://img.shields.io/badge/rust-docs-blue.svg?style=flat-square)](https://docs.rs/crate/multiaddr) +[![crates.io](https://img.shields.io/badge/crates.io-v0.2.0-orange.svg?style=flat-square )](https://crates.io/crates/multiaddr) +[![](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) + + +> [multiaddr](https://github.com/multiformats/multiaddr) implementation in Rust. + +## Table of Contents + +- [Install](#install) +- [Usage](#usage) +- [Maintainers](#maintainers) +- [Contribute](#contribute) +- [License](#license) + +## Install + +First add this to your `Cargo.toml` + +```toml +[dependencies] +multiaddr = "*" +``` + +then run `cargo build`. + +## Usage + +```rust +extern crate multiaddr; + +use multiaddr::{Multiaddr, multiaddr}; + +let address = "/ip4/127.0.0.1/tcp/1234".parse::().unwrap(); +// or with a macro +let other = multiaddr!(Ip4([127, 0, 0, 1]), Udp(10500u16), QuicV1); + +assert_eq!(address.to_string(), "/ip4/127.0.0.1/tcp/1234"); +assert_eq!(other.to_string(), "/ip4/127.0.0.1/udp/10500/quic-v1"); +``` + +## Maintainers + +Captain: [@dignifiedquire](https://github.com/dignifiedquire). + +## Contribute + +Contributions welcome. Please check out [the issues](https://github.com/multiformats/rust-multiaddr/issues). + +Check out our [contributing document](https://github.com/multiformats/multiformats/blob/master/contributing.md) for more information on how we work, and about contributing in general. Please be aware that all interactions related to multiformats are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Small note: If editing the README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. + +## License + +[MIT](LICENSE) © 2015-2017 Friedel Ziegelmeyer diff --git a/multiaddr/src/errors.rs b/multiaddr/src/errors.rs new file mode 100644 index 00000000000..d3ded906a9e --- /dev/null +++ b/multiaddr/src/errors.rs @@ -0,0 +1,92 @@ +use std::{error, fmt, io, net, num, str, string}; +use unsigned_varint::decode; + +pub type Result = ::std::result::Result; + +/// Error types +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + DataLessThanLen, + InvalidMultiaddr, + InvalidProtocolString, + InvalidUvar(decode::Error), + ParsingError(Box), + UnknownProtocolId(u32), + UnknownProtocolString(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::DataLessThanLen => f.write_str("we have less data than indicated by length"), + Error::InvalidMultiaddr => f.write_str("invalid multiaddr"), + Error::InvalidProtocolString => f.write_str("invalid protocol string"), + Error::InvalidUvar(e) => write!(f, "failed to decode unsigned varint: {e}"), + Error::ParsingError(e) => write!(f, "failed to parse: {e}"), + Error::UnknownProtocolId(id) => write!(f, "unknown protocol id: {id}"), + Error::UnknownProtocolString(string) => { + write!(f, "unknown protocol string: {string}") + } + } + } +} + +impl error::Error for Error { + #[inline] + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + if let Error::ParsingError(e) = self { + Some(&**e) + } else { + None + } + } +} + +impl From for Error { + fn from(err: io::Error) -> Error { + Error::ParsingError(err.into()) + } +} + +impl From for Error { + fn from(err: multihash::Error) -> Error { + Error::ParsingError(err.into()) + } +} + +impl From for Error { + fn from(err: multibase::Error) -> Error { + Error::ParsingError(err.into()) + } +} + +impl From for Error { + fn from(err: net::AddrParseError) -> Error { + Error::ParsingError(err.into()) + } +} + +impl From for Error { + fn from(err: num::ParseIntError) -> Error { + Error::ParsingError(err.into()) + } +} + +impl From for Error { + fn from(err: string::FromUtf8Error) -> Error { + Error::ParsingError(err.into()) + } +} + +impl From for Error { + fn from(err: str::Utf8Error) -> Error { + Error::ParsingError(err.into()) + } +} + +impl From for Error { + fn from(e: decode::Error) -> Error { + Error::InvalidUvar(e) + } +} diff --git a/multiaddr/src/from_url.rs b/multiaddr/src/from_url.rs new file mode 100644 index 00000000000..ce5ae6d579f --- /dev/null +++ b/multiaddr/src/from_url.rs @@ -0,0 +1,298 @@ +use crate::{Multiaddr, Protocol}; +use std::{error, fmt, iter, net::IpAddr}; + +/// Attempts to parse an URL into a multiaddress. +/// +/// This function will return an error if some information in the URL cannot be retained in the +/// generated multiaddress. This includes a username, password, path (if not supported by the +/// multiaddr), and query string. +/// +/// This function is only present if the `url` feature is enabled, and it is +/// enabled by default. +/// +/// The supported URL schemes are: +/// +/// - `ws://example.com/` +/// - `wss://example.com/` +/// - `http://example.com/` +/// - `https://example.com/` +/// - `unix:/foo/bar` +/// +/// # Example +/// +/// ``` +/// let addr = multiaddr::from_url("ws://127.0.0.1:8080/").unwrap(); +/// assert_eq!(addr, "/ip4/127.0.0.1/tcp/8080/ws".parse().unwrap()); +/// ``` +/// +pub fn from_url(url: &str) -> std::result::Result { + from_url_inner(url, false) +} + +/// Attempts to parse an URL into a multiaddress. Ignores possible loss of information. +/// +/// This function is similar to [`from_url`], except that we don't return an error if some +/// information in the URL cannot be retain in the generated multiaddress. +/// +/// This function is only present if the `url` feature is enabled, and it is +/// enabled by default. +/// +/// # Example +/// +/// ``` +/// let addr = "ws://user:pass@127.0.0.1:8080/"; +/// assert!(multiaddr::from_url(addr).is_err()); +/// assert!(multiaddr::from_url_lossy(addr).is_ok()); +/// ``` +/// +pub fn from_url_lossy(url: &str) -> std::result::Result { + from_url_inner(url, true) +} + +/// Underlying implementation of `from_url` and `from_url_lossy`. +fn from_url_inner(url: &str, lossy: bool) -> std::result::Result { + let url = url::Url::parse(url).map_err(|_| FromUrlErr::BadUrl)?; + + match url.scheme() { + // Note: if you add support for a new scheme, please update the documentation as well. + "ws" | "wss" | "http" | "https" => from_url_inner_http_ws(url, lossy), + "unix" => from_url_inner_path(url, lossy), + _ => Err(FromUrlErr::UnsupportedScheme), + } +} + +/// Called when `url.scheme()` is an Internet-like URL. +fn from_url_inner_http_ws( + url: url::Url, + lossy: bool, +) -> std::result::Result { + let (protocol, lost_path, default_port) = match url.scheme() { + "ws" => (Protocol::Ws(url.path().to_owned().into()), false, 80), + "wss" => (Protocol::Wss(url.path().to_owned().into()), false, 443), + "http" => (Protocol::Http, true, 80), + "https" => (Protocol::Https, true, 443), + _ => unreachable!("We only call this function for one of the given schemes; qed"), + }; + + let port = Protocol::Tcp(url.port().unwrap_or(default_port)); + let ip = if let Some(hostname) = url.host_str() { + if let Ok(ip) = hostname.parse::() { + Protocol::from(ip) + } else { + Protocol::Dns(hostname.into()) + } + } else { + return Err(FromUrlErr::BadUrl); + }; + + if !lossy + && (!url.username().is_empty() + || url.password().is_some() + || (lost_path && url.path() != "/" && !url.path().is_empty()) + || url.query().is_some() + || url.fragment().is_some()) + { + return Err(FromUrlErr::InformationLoss); + } + + Ok(iter::once(ip) + .chain(iter::once(port)) + .chain(iter::once(protocol)) + .collect()) +} + +/// Called when `url.scheme()` is a path-like URL. +fn from_url_inner_path(url: url::Url, lossy: bool) -> std::result::Result { + let protocol = match url.scheme() { + "unix" => Protocol::Unix(url.path().to_owned().into()), + _ => unreachable!("We only call this function for one of the given schemes; qed"), + }; + + if !lossy + && (!url.username().is_empty() + || url.password().is_some() + || url.query().is_some() + || url.fragment().is_some()) + { + return Err(FromUrlErr::InformationLoss); + } + + Ok(Multiaddr::from(protocol)) +} + +/// Error while parsing an URL. +#[derive(Debug)] +pub enum FromUrlErr { + /// Failed to parse the URL. + BadUrl, + /// The URL scheme was not recognized. + UnsupportedScheme, + /// Some information in the URL would be lost. Never returned by `from_url_lossy`. + InformationLoss, +} + +impl fmt::Display for FromUrlErr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FromUrlErr::BadUrl => write!(f, "Bad URL"), + FromUrlErr::UnsupportedScheme => write!(f, "Unrecognized URL scheme"), + FromUrlErr::InformationLoss => write!(f, "Some information in the URL would be lost"), + } + } +} + +impl error::Error for FromUrlErr {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_garbage_doesnt_panic() { + for _ in 0..50 { + let url = (0..16).map(|_| rand::random::()).collect::>(); + let url = String::from_utf8_lossy(&url); + assert!(from_url(&url).is_err()); + } + } + + #[test] + fn normal_usage_ws() { + let addr = from_url("ws://127.0.0.1:8000").unwrap(); + assert_eq!(addr, "/ip4/127.0.0.1/tcp/8000/ws".parse().unwrap()); + } + + #[test] + fn normal_usage_wss() { + let addr = from_url("wss://127.0.0.1:8000").unwrap(); + assert_eq!(addr, "/ip4/127.0.0.1/tcp/8000/wss".parse().unwrap()); + } + + #[test] + fn default_ws_port() { + let addr = from_url("ws://127.0.0.1").unwrap(); + assert_eq!(addr, "/ip4/127.0.0.1/tcp/80/ws".parse().unwrap()); + } + + #[test] + fn default_http_port() { + let addr = from_url("http://127.0.0.1").unwrap(); + assert_eq!(addr, "/ip4/127.0.0.1/tcp/80/http".parse().unwrap()); + } + + #[test] + fn default_wss_port() { + let addr = from_url("wss://127.0.0.1").unwrap(); + assert_eq!(addr, "/ip4/127.0.0.1/tcp/443/wss".parse().unwrap()); + } + + #[test] + fn default_https_port() { + let addr = from_url("https://127.0.0.1").unwrap(); + assert_eq!(addr, "/ip4/127.0.0.1/tcp/443/https".parse().unwrap()); + } + + #[test] + fn dns_addr_ws() { + let addr = from_url("ws://example.com").unwrap(); + assert_eq!(addr, "/dns/example.com/tcp/80/ws".parse().unwrap()); + } + + #[test] + fn dns_addr_http() { + let addr = from_url("http://example.com").unwrap(); + assert_eq!(addr, "/dns/example.com/tcp/80/http".parse().unwrap()); + } + + #[test] + fn dns_addr_wss() { + let addr = from_url("wss://example.com").unwrap(); + assert_eq!(addr, "/dns/example.com/tcp/443/wss".parse().unwrap()); + } + + #[test] + fn dns_addr_https() { + let addr = from_url("https://example.com").unwrap(); + assert_eq!(addr, "/dns/example.com/tcp/443/https".parse().unwrap()); + } + + #[test] + fn bad_hostname() { + let addr = from_url("wss://127.0.0.1x").unwrap(); + assert_eq!(addr, "/dns/127.0.0.1x/tcp/443/wss".parse().unwrap()); + } + + #[test] + fn wrong_scheme() { + match from_url("foo://127.0.0.1") { + Err(FromUrlErr::UnsupportedScheme) => {} + _ => panic!(), + } + } + + #[test] + fn dns_and_port() { + let addr = from_url("http://example.com:1000").unwrap(); + assert_eq!(addr, "/dns/example.com/tcp/1000/http".parse().unwrap()); + } + + #[test] + fn username_lossy() { + let addr = "http://foo@example.com:1000/"; + assert!(from_url(addr).is_err()); + assert!(from_url_lossy(addr).is_ok()); + assert!(from_url("http://@example.com:1000/").is_ok()); + } + + #[test] + fn password_lossy() { + let addr = "http://:bar@example.com:1000/"; + assert!(from_url(addr).is_err()); + assert!(from_url_lossy(addr).is_ok()); + } + + #[test] + fn path_lossy() { + let addr = "http://example.com:1000/foo"; + assert!(from_url(addr).is_err()); + assert!(from_url_lossy(addr).is_ok()); + } + + #[test] + fn fragment_lossy() { + let addr = "http://example.com:1000/#foo"; + assert!(from_url(addr).is_err()); + assert!(from_url_lossy(addr).is_ok()); + } + + #[test] + fn unix() { + let addr = from_url("unix:/foo/bar").unwrap(); + assert_eq!(addr, Multiaddr::from(Protocol::Unix("/foo/bar".into()))); + } + + #[test] + fn ws_path() { + let addr = from_url("ws://1.2.3.4:1000/foo/bar").unwrap(); + assert_eq!( + addr, + "/ip4/1.2.3.4/tcp/1000/x-parity-ws/%2ffoo%2fbar" + .parse() + .unwrap() + ); + + let addr = from_url("ws://1.2.3.4:1000/").unwrap(); + assert_eq!(addr, "/ip4/1.2.3.4/tcp/1000/ws".parse().unwrap()); + + let addr = from_url("wss://1.2.3.4:1000/foo/bar").unwrap(); + assert_eq!( + addr, + "/ip4/1.2.3.4/tcp/1000/x-parity-wss/%2ffoo%2fbar" + .parse() + .unwrap() + ); + + let addr = from_url("wss://1.2.3.4:1000").unwrap(); + assert_eq!(addr, "/ip4/1.2.3.4/tcp/1000/wss".parse().unwrap()); + } +} diff --git a/multiaddr/src/lib.rs b/multiaddr/src/lib.rs new file mode 100644 index 00000000000..b6b0ad4c9de --- /dev/null +++ b/multiaddr/src/lib.rs @@ -0,0 +1,511 @@ +//! Implementation of [multiaddr](https://github.com/multiformats/multiaddr) in Rust. +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +pub use multihash; + +mod errors; +mod onion_addr; +mod protocol; + +#[cfg(feature = "url")] +mod from_url; + +pub use self::errors::{Error, Result}; +pub use self::onion_addr::Onion3Addr; +pub use self::protocol::Protocol; +use bytes::{BufMut, Bytes, BytesMut}; +use serde::{ + de::{self, Error as DeserializerError}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use std::{ + convert::TryFrom, + fmt, + iter::FromIterator, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + result::Result as StdResult, + str::FromStr, +}; + +pub use libp2p_identity::PeerId; + +#[cfg(feature = "url")] +pub use self::from_url::{from_url, from_url_lossy, FromUrlErr}; + +static_assertions::const_assert! { + // This check is most certainly overkill right now, but done here + // anyway to ensure the `as u64` casts in this crate are safe. + std::mem::size_of::() <= std::mem::size_of::() +} + +/// Representation of a Multiaddr. +#[allow(clippy::rc_buffer)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] +pub struct Multiaddr { + bytes: Bytes, +} + +impl Multiaddr { + /// Create a new, empty multiaddress. + pub fn empty() -> Self { + Self { + bytes: Bytes::new(), + } + } + + /// Create a new, empty multiaddress with the given capacity. + pub fn with_capacity(n: usize) -> Self { + Self { + bytes: BytesMut::with_capacity(n).freeze(), + } + } + + /// Return the length in bytes of this multiaddress. + pub fn len(&self) -> usize { + self.bytes.len() + } + + /// Returns true if the length of this multiaddress is 0. + pub fn is_empty(&self) -> bool { + self.bytes.len() == 0 + } + + /// Return a copy of this [`Multiaddr`]'s byte representation. + pub fn to_vec(&self) -> Vec { + Vec::from(&self.bytes[..]) + } + + /// Adds an already-parsed address component to the end of this multiaddr. + /// + /// # Examples + /// + /// ``` + /// use multiaddr::{Multiaddr, Protocol}; + /// + /// let mut address: Multiaddr = "/ip4/127.0.0.1".parse().unwrap(); + /// address.push(Protocol::Tcp(10000)); + /// assert_eq!(address, "/ip4/127.0.0.1/tcp/10000".parse().unwrap()); + /// ``` + /// + pub fn push(&mut self, p: Protocol<'_>) { + let mut bytes = BytesMut::from(std::mem::take(&mut self.bytes)); + p.write_bytes(&mut (&mut bytes).writer()) + .expect("Writing to a `BytesMut` never fails."); + self.bytes = bytes.freeze(); + } + + /// Pops the last `Protocol` of this multiaddr, or `None` if the multiaddr is empty. + /// ``` + /// use multiaddr::{Multiaddr, Protocol}; + /// + /// let mut address: Multiaddr = "/ip4/127.0.0.1/udt/sctp/5678".parse().unwrap(); + /// + /// assert_eq!(address.pop().unwrap(), Protocol::Sctp(5678)); + /// assert_eq!(address.pop().unwrap(), Protocol::Udt); + /// ``` + /// + pub fn pop<'a>(&mut self) -> Option> { + let mut slice = &self.bytes[..]; // the remaining multiaddr slice + if slice.is_empty() { + return None; + } + let protocol = loop { + let (p, s) = Protocol::from_bytes(slice).expect("`slice` is a valid `Protocol`."); + if s.is_empty() { + break p.acquire(); + } + slice = s + }; + let remaining_len = self.len() - slice.len(); + let mut bytes = BytesMut::from(std::mem::take(&mut self.bytes)); + bytes.truncate(remaining_len); + self.bytes = bytes.freeze(); + Some(protocol) + } + + /// Like [`Multiaddr::push`] but consumes `self`. + pub fn with(mut self, p: Protocol<'_>) -> Self { + let mut bytes = BytesMut::from(std::mem::take(&mut self.bytes)); + p.write_bytes(&mut (&mut bytes).writer()) + .expect("Writing to a `BytesMut` never fails."); + self.bytes = bytes.freeze(); + self + } + + /// Appends the given [`PeerId`] if not yet present at the end of this multiaddress. + /// + /// Fails if this address ends in a _different_ [`PeerId`]. + /// In that case, the original, unmodified address is returned. + pub fn with_p2p(self, peer: PeerId) -> std::result::Result { + match self.iter().last() { + Some(Protocol::P2p(p)) if p == peer => Ok(self), + Some(Protocol::P2p(_)) => Err(self), + _ => Ok(self.with(Protocol::P2p(peer))), + } + } + + /// Returns the components of this multiaddress. + /// + /// # Example + /// + /// ```rust + /// use std::net::Ipv4Addr; + /// use multiaddr::{Multiaddr, Protocol}; + /// + /// let address: Multiaddr = "/ip4/127.0.0.1/udt/sctp/5678".parse().unwrap(); + /// + /// let components = address.iter().collect::>(); + /// assert_eq!(components[0], Protocol::Ip4(Ipv4Addr::new(127, 0, 0, 1))); + /// assert_eq!(components[1], Protocol::Udt); + /// assert_eq!(components[2], Protocol::Sctp(5678)); + /// ``` + /// + pub fn iter(&self) -> Iter<'_> { + Iter(&self.bytes) + } + + /// Replace a [`Protocol`] at some position in this `Multiaddr`. + /// + /// The parameter `at` denotes the index of the protocol at which the function + /// `by` will be applied to the current protocol, returning an optional replacement. + /// + /// If `at` is out of bounds or `by` does not yield a replacement value, + /// `None` will be returned. Otherwise a copy of this `Multiaddr` with the + /// updated `Protocol` at position `at` will be returned. + pub fn replace<'a, F>(&self, at: usize, by: F) -> Option + where + F: FnOnce(&Protocol<'_>) -> Option>, + { + let mut address = Multiaddr::with_capacity(self.len()); + let mut fun = Some(by); + let mut replaced = false; + + for (i, p) in self.iter().enumerate() { + if i == at { + let f = fun.take().expect("i == at only happens once"); + if let Some(q) = f(&p) { + address = address.with(q); + replaced = true; + continue; + } + return None; + } + address = address.with(p) + } + + if replaced { + Some(address) + } else { + None + } + } + + /// Checks whether the given `Multiaddr` is a suffix of this `Multiaddr`. + pub fn ends_with(&self, other: &Multiaddr) -> bool { + let n = self.bytes.len(); + let m = other.bytes.len(); + if n < m { + return false; + } + self.bytes[(n - m)..] == other.bytes[..] + } + + /// Checks whether the given `Multiaddr` is a prefix of this `Multiaddr`. + pub fn starts_with(&self, other: &Multiaddr) -> bool { + let n = self.bytes.len(); + let m = other.bytes.len(); + if n < m { + return false; + } + self.bytes[..m] == other.bytes[..] + } + + /// Returns &str identifiers for the protocol names themselves. + /// This omits specific info like addresses, ports, peer IDs, and the like. + /// Example: `"/ip4/127.0.0.1/tcp/5001"` would return `["ip4", "tcp"]` + pub fn protocol_stack(&self) -> ProtoStackIter { + ProtoStackIter { parts: self.iter() } + } +} + +impl fmt::Debug for Multiaddr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl fmt::Display for Multiaddr { + /// Convert a Multiaddr to a string + /// + /// # Example + /// + /// ``` + /// use multiaddr::Multiaddr; + /// + /// let address: Multiaddr = "/ip4/127.0.0.1/udt".parse().unwrap(); + /// assert_eq!(address.to_string(), "/ip4/127.0.0.1/udt"); + /// ``` + /// + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for s in self.iter() { + s.fmt(f)?; + } + Ok(()) + } +} + +impl AsRef<[u8]> for Multiaddr { + fn as_ref(&self) -> &[u8] { + self.bytes.as_ref() + } +} + +impl<'a> IntoIterator for &'a Multiaddr { + type Item = Protocol<'a>; + type IntoIter = Iter<'a>; + + fn into_iter(self) -> Iter<'a> { + Iter(&self.bytes) + } +} + +impl<'a> FromIterator> for Multiaddr { + fn from_iter(iter: T) -> Self + where + T: IntoIterator>, + { + let mut bytes = BytesMut::new(); + for cmp in iter { + cmp.write_bytes(&mut (&mut bytes).writer()) + .expect("Writing to a `BytesMut` never fails."); + } + Multiaddr { + bytes: bytes.freeze(), + } + } +} + +impl FromStr for Multiaddr { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut bytes = BytesMut::new(); + let mut parts = input.split('/').peekable(); + + if Some("") != parts.next() { + // A multiaddr must start with `/` + return Err(Error::InvalidMultiaddr); + } + + while parts.peek().is_some() { + let p = Protocol::from_str_parts(&mut parts)?; + p.write_bytes(&mut (&mut bytes).writer()) + .expect("Writing to a `BytesMut` never fails."); + } + + Ok(Multiaddr { + bytes: bytes.freeze(), + }) + } +} + +/// Iterator over `Multiaddr` [`Protocol`]s. +pub struct Iter<'a>(&'a [u8]); + +impl<'a> Iterator for Iter<'a> { + type Item = Protocol<'a>; + + fn next(&mut self) -> Option { + if self.0.is_empty() { + return None; + } + + let (p, next_data) = + Protocol::from_bytes(self.0).expect("`Multiaddr` is known to be valid."); + + self.0 = next_data; + Some(p) + } +} + +/// Iterator over the string identifiers of the protocols (not addrs) in a multiaddr +pub struct ProtoStackIter<'a> { + parts: Iter<'a>, +} + +impl Iterator for ProtoStackIter<'_> { + type Item = &'static str; + fn next(&mut self) -> Option { + self.parts.next().as_ref().map(Protocol::tag) + } +} + +impl<'a> From> for Multiaddr { + fn from(p: Protocol<'a>) -> Multiaddr { + let mut bytes = BytesMut::new(); + p.write_bytes(&mut (&mut bytes).writer()) + .expect("Writing to a `BytesMut` never fails."); + Multiaddr { + bytes: bytes.freeze(), + } + } +} + +impl From for Multiaddr { + fn from(v: IpAddr) -> Multiaddr { + match v { + IpAddr::V4(a) => a.into(), + IpAddr::V6(a) => a.into(), + } + } +} + +impl From for Multiaddr { + fn from(v: Ipv4Addr) -> Multiaddr { + Protocol::Ip4(v).into() + } +} + +impl From for Multiaddr { + fn from(v: Ipv6Addr) -> Multiaddr { + Protocol::Ip6(v).into() + } +} + +impl TryFrom> for Multiaddr { + type Error = Error; + + fn try_from(v: Vec) -> Result { + // Check if the argument is a valid `Multiaddr` by reading its protocols. + let mut slice = &v[..]; + while !slice.is_empty() { + let (_, s) = Protocol::from_bytes(slice)?; + slice = s + } + Ok(Multiaddr { + bytes: Bytes::from(v), + }) + } +} + +impl TryFrom for Multiaddr { + type Error = Error; + + fn try_from(s: String) -> Result { + s.parse() + } +} + +impl<'a> TryFrom<&'a str> for Multiaddr { + type Error = Error; + + fn try_from(s: &'a str) -> Result { + s.parse() + } +} + +impl Serialize for Multiaddr { + fn serialize(&self, serializer: S) -> StdResult + where + S: Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&self.to_string()) + } else { + serializer.serialize_bytes(self.as_ref()) + } + } +} + +impl<'de> Deserialize<'de> for Multiaddr { + fn deserialize(deserializer: D) -> StdResult + where + D: Deserializer<'de>, + { + struct Visitor { + is_human_readable: bool, + } + + impl<'de> de::Visitor<'de> for Visitor { + type Value = Multiaddr; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("multiaddress") + } + fn visit_seq>( + self, + mut seq: A, + ) -> StdResult { + let mut buf: Vec = + Vec::with_capacity(std::cmp::min(seq.size_hint().unwrap_or(0), 4096)); + while let Some(e) = seq.next_element()? { + buf.push(e); + } + if self.is_human_readable { + let s = String::from_utf8(buf).map_err(DeserializerError::custom)?; + s.parse().map_err(DeserializerError::custom) + } else { + Multiaddr::try_from(buf).map_err(DeserializerError::custom) + } + } + fn visit_str(self, v: &str) -> StdResult { + v.parse().map_err(DeserializerError::custom) + } + fn visit_borrowed_str(self, v: &'de str) -> StdResult { + self.visit_str(v) + } + fn visit_string(self, v: String) -> StdResult { + self.visit_str(&v) + } + fn visit_bytes(self, v: &[u8]) -> StdResult { + self.visit_byte_buf(v.into()) + } + fn visit_borrowed_bytes(self, v: &'de [u8]) -> StdResult { + self.visit_byte_buf(v.into()) + } + fn visit_byte_buf(self, v: Vec) -> StdResult { + Multiaddr::try_from(v).map_err(DeserializerError::custom) + } + } + + if deserializer.is_human_readable() { + deserializer.deserialize_str(Visitor { + is_human_readable: true, + }) + } else { + deserializer.deserialize_bytes(Visitor { + is_human_readable: false, + }) + } + } +} + +/// Easy way for a user to create a `Multiaddr`. +/// +/// Example: +/// +/// ```rust +/// # use multiaddr::multiaddr; +/// let addr = multiaddr!(Ip4([127, 0, 0, 1]), Tcp(10500u16)); +/// ``` +/// +/// Each element passed to `multiaddr!` should be a variant of the `Protocol` enum. The +/// optional parameter is turned into the proper type with the `Into` trait. +/// +/// For example, `Ip4([127, 0, 0, 1])` works because `Ipv4Addr` implements `From<[u8; 4]>`. +#[macro_export] +macro_rules! multiaddr { + ($($comp:ident $(($param:expr))*),+) => { + { + use std::iter; + let elem = iter::empty::<$crate::Protocol>(); + $( + let elem = { + let cmp = $crate::Protocol::$comp $(( $param.into() ))*; + elem.chain(iter::once(cmp)) + }; + )+ + elem.collect::<$crate::Multiaddr>() + } + } +} diff --git a/multiaddr/src/onion_addr.rs b/multiaddr/src/onion_addr.rs new file mode 100644 index 00000000000..38004523e0b --- /dev/null +++ b/multiaddr/src/onion_addr.rs @@ -0,0 +1,51 @@ +use std::{borrow::Cow, fmt}; + +/// Represents an Onion v3 address +#[derive(Clone)] +pub struct Onion3Addr<'a>(Cow<'a, [u8; 35]>, u16); + +impl Onion3Addr<'_> { + /// Return the hash of the public key as bytes + pub fn hash(&self) -> &[u8; 35] { + self.0.as_ref() + } + + /// Return the port + pub fn port(&self) -> u16 { + self.1 + } + + /// Consume this instance and create an owned version containing the same address + pub fn acquire<'b>(self) -> Onion3Addr<'b> { + Onion3Addr(Cow::Owned(self.0.into_owned()), self.1) + } +} + +impl PartialEq for Onion3Addr<'_> { + fn eq(&self, other: &Self) -> bool { + self.1 == other.1 && self.0[..] == other.0[..] + } +} + +impl Eq for Onion3Addr<'_> {} + +impl From<([u8; 35], u16)> for Onion3Addr<'_> { + fn from(parts: ([u8; 35], u16)) -> Self { + Self(Cow::Owned(parts.0), parts.1) + } +} + +impl<'a> From<(&'a [u8; 35], u16)> for Onion3Addr<'a> { + fn from(parts: (&'a [u8; 35], u16)) -> Self { + Self(Cow::Borrowed(parts.0), parts.1) + } +} + +impl fmt::Debug for Onion3Addr<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.debug_tuple("Onion3Addr") + .field(&format!("{:02x?}", &self.0[..])) + .field(&self.1) + .finish() + } +} diff --git a/multiaddr/src/protocol.rs b/multiaddr/src/protocol.rs new file mode 100644 index 00000000000..f0fa1d40fef --- /dev/null +++ b/multiaddr/src/protocol.rs @@ -0,0 +1,867 @@ +use crate::onion_addr::Onion3Addr; +use crate::{Error, PeerId, Result}; +use arrayref::array_ref; +use byteorder::{BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt}; +use data_encoding::BASE32; +use std::{ + borrow::Cow, + convert::From, + fmt, + io::{Cursor, Write}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::{self, FromStr}, +}; +use unsigned_varint::{decode, encode}; + +// All the values are obtained by converting hexadecimal protocol codes to u32. +// Protocols as well as their corresponding codes are defined in +// https://github.com/multiformats/multiaddr/blob/master/protocols.csv . +const DCCP: u32 = 33; +const DNS: u32 = 53; +const DNS4: u32 = 54; +const DNS6: u32 = 55; +const DNSADDR: u32 = 56; +const HTTP: u32 = 480; +const HTTPS: u32 = 443; // Deprecated - alias for /tls/http +const IP4: u32 = 4; +const IP6: u32 = 41; +const P2P_WEBRTC_DIRECT: u32 = 276; // Deprecated +const P2P_WEBRTC_STAR: u32 = 275; // Deprecated +const WEBRTC_DIRECT: u32 = 280; +const CERTHASH: u32 = 466; +const P2P_WEBSOCKET_STAR: u32 = 479; // Deprecated +const MEMORY: u32 = 777; +const ONION: u32 = 444; +const ONION3: u32 = 445; +const P2P: u32 = 421; +const P2P_CIRCUIT: u32 = 290; +const QUIC: u32 = 460; +const QUIC_V1: u32 = 461; +const SCTP: u32 = 132; +const TCP: u32 = 6; +const TLS: u32 = 448; +const NOISE: u32 = 454; +const UDP: u32 = 273; +const UDT: u32 = 301; +const UNIX: u32 = 400; +const UTP: u32 = 302; +const WEBTRANSPORT: u32 = 465; +const WS: u32 = 477; +const WS_WITH_PATH: u32 = 4770; // Note: not standard +const WSS: u32 = 478; // Deprecated - alias for /tls/ws +const WSS_WITH_PATH: u32 = 4780; // Note: not standard +const IP6ZONE: u32 = 42; +const IPCIDR: u32 = 43; +// const IPFS: u32 = 421; // Deprecated +const GARLIC64: u32 = 446; +const GARLIC32: u32 = 447; +const SNI: u32 = 449; +const P2P_STARDUST: u32 = 277; // Deprecated +const WEBRTC: u32 = 281; +const HTTP_PATH: u32 = 481; + +/// Type-alias for how multi-addresses use `Multihash`. +/// +/// The `64` defines the allocation size for the digest within the `Multihash`. +/// This allows us to use hashes such as SHA512. +/// In case protocols like `/certhash` ever support hashes larger than that, we will need to update this size here (which will be a breaking change!). +type Multihash = multihash::Multihash<64>; + +const PATH_SEGMENT_ENCODE_SET: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS + .add(b'%') + .add(b'/') + .add(b'`') + .add(b'?') + .add(b'{') + .add(b'}') + .add(b' ') + .add(b'"') + .add(b'#') + .add(b'<') + .add(b'>'); + +/// `Protocol` describes all possible multiaddress protocols. +/// +/// For `Unix`, `Ws` and `Wss` we use `&str` instead of `Path` to allow +/// cross-platform usage of `Protocol` since encoding `Paths` to bytes is +/// platform-specific. This means that the actual validation of paths needs to +/// happen separately. +#[derive(PartialEq, Eq, Clone, Debug)] +#[non_exhaustive] +pub enum Protocol<'a> { + Dccp(u16), + Dns(Cow<'a, str>), + Dns4(Cow<'a, str>), + Dns6(Cow<'a, str>), + Dnsaddr(Cow<'a, str>), + Http, + Https, + Ip4(Ipv4Addr), + Ip6(Ipv6Addr), + P2pWebRtcDirect, + P2pWebRtcStar, + WebRTCDirect, + Certhash(Multihash), + P2pWebSocketStar, + /// Contains the "port" to contact. Similar to TCP or UDP, 0 means "assign me a port". + Memory(u64), + Onion(Cow<'a, [u8; 10]>, u16), + Onion3(Onion3Addr<'a>), + P2p(PeerId), + P2pCircuit, + Quic, + QuicV1, + Sctp(u16), + Tcp(u16), + Tls, + Noise, + Udp(u16), + Udt, + Unix(Cow<'a, str>), + Utp, + WebTransport, + Ws(Cow<'a, str>), + Wss(Cow<'a, str>), + Ip6zone(Cow<'a, str>), + Ipcidr(u8), + Garlic64(Cow<'a, [u8]>), + Garlic32(Cow<'a, [u8]>), + Sni(Cow<'a, str>), + P2pStardust, + WebRTC, + HttpPath(Cow<'a, str>), +} + +impl<'a> Protocol<'a> { + /// Parse a protocol value from the given iterator of string slices. + /// + /// The parsing only consumes the minimum amount of string slices necessary to + /// produce a well-formed protocol. The same iterator can thus be used to parse + /// a sequence of protocols in succession. It is up to client code to check + /// that iteration has finished whenever appropriate. + pub fn from_str_parts(mut iter: I) -> Result + where + I: Iterator, + { + match iter.next().ok_or(Error::InvalidProtocolString)? { + "ip4" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Ip4(Ipv4Addr::from_str(s)?)) + } + "tcp" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Tcp(s.parse()?)) + } + "tls" => Ok(Protocol::Tls), + "noise" => Ok(Protocol::Noise), + "udp" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Udp(s.parse()?)) + } + "dccp" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Dccp(s.parse()?)) + } + "ip6" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Ip6(Ipv6Addr::from_str(s)?)) + } + "dns" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Dns(Cow::Borrowed(s))) + } + "dns4" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Dns4(Cow::Borrowed(s))) + } + "dns6" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Dns6(Cow::Borrowed(s))) + } + "dnsaddr" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Dnsaddr(Cow::Borrowed(s))) + } + "sctp" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Sctp(s.parse()?)) + } + "udt" => Ok(Protocol::Udt), + "utp" => Ok(Protocol::Utp), + "unix" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Unix(Cow::Borrowed(s))) + } + "p2p" | "ipfs" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + let decoded = multibase::Base::Base58Btc.decode(s)?; + let peer_id = + PeerId::from_bytes(&decoded).map_err(|e| Error::ParsingError(Box::new(e)))?; + Ok(Protocol::P2p(peer_id)) + } + "http" => Ok(Protocol::Http), + "https" => Ok(Protocol::Https), + "onion" => iter + .next() + .ok_or(Error::InvalidProtocolString) + .and_then(|s| read_onion(&s.to_uppercase())) + .map(|(a, p)| Protocol::Onion(Cow::Owned(a), p)), + "onion3" => iter + .next() + .ok_or(Error::InvalidProtocolString) + .and_then(|s| read_onion3(&s.to_uppercase())) + .map(|(a, p)| Protocol::Onion3((a, p).into())), + "quic" => Ok(Protocol::Quic), + "quic-v1" => Ok(Protocol::QuicV1), + "webtransport" => Ok(Protocol::WebTransport), + "ws" => Ok(Protocol::Ws(Cow::Borrowed("/"))), + "wss" => Ok(Protocol::Wss(Cow::Borrowed("/"))), + "x-parity-ws" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + let decoded = percent_encoding::percent_decode(s.as_bytes()).decode_utf8()?; + Ok(Protocol::Ws(decoded)) + } + "x-parity-wss" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + let decoded = percent_encoding::percent_decode(s.as_bytes()).decode_utf8()?; + Ok(Protocol::Wss(decoded)) + } + "p2p-websocket-star" => Ok(Protocol::P2pWebSocketStar), + "p2p-webrtc-star" => Ok(Protocol::P2pWebRtcStar), + "webrtc-direct" => Ok(Protocol::WebRTCDirect), + "certhash" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + let (_base, decoded) = multibase::decode(s)?; + Ok(Protocol::Certhash(Multihash::from_bytes(&decoded)?)) + } + "p2p-webrtc-direct" => Ok(Protocol::P2pWebRtcDirect), + "p2p-circuit" => Ok(Protocol::P2pCircuit), + "memory" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Memory(s.parse()?)) + } + "ip6zone" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Ip6zone(Cow::Borrowed(s))) + } + "ipcidr" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Ipcidr(s.parse()?)) + } + "garlic64" => { + let s = iter + .next() + .ok_or(Error::InvalidProtocolString)? + .replace('-', "+") + .replace('~', "/"); + + if s.len() < 516 || s.len() > 616 { + return Err(Error::InvalidProtocolString); + } + + let decoded = multibase::Base::Base64.decode(s)?; + Ok(Protocol::Garlic64(Cow::from(decoded))) + } + "garlic32" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + + if s.len() < 55 && s.len() != 52 { + return Err(Error::InvalidProtocolString); + } + + let decoded = multibase::Base::Base32Lower.decode(s)?; + Ok(Protocol::Garlic32(Cow::from(decoded))) + } + "sni" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + Ok(Protocol::Sni(Cow::Borrowed(s))) + } + "p2p-stardust" => Ok(Protocol::P2pStardust), + "webrtc" => Ok(Protocol::WebRTC), + "http-path" => { + let s = iter.next().ok_or(Error::InvalidProtocolString)?; + let decoded = percent_encoding::percent_decode(s.as_bytes()).decode_utf8()?; + Ok(Protocol::HttpPath(decoded)) + } + unknown => Err(Error::UnknownProtocolString(unknown.to_string())), + } + } + + /// Parse a single `Protocol` value from its byte slice representation, + /// returning the protocol as well as the remaining byte slice. + pub fn from_bytes(input: &'a [u8]) -> Result<(Self, &'a [u8])> { + fn split_at(n: usize, input: &[u8]) -> Result<(&[u8], &[u8])> { + if input.len() < n { + return Err(Error::DataLessThanLen); + } + Ok(input.split_at(n)) + } + let (id, input) = decode::u32(input)?; + match id { + DCCP => { + let (data, rest) = split_at(2, input)?; + let mut rdr = Cursor::new(data); + let num = rdr.read_u16::()?; + Ok((Protocol::Dccp(num), rest)) + } + DNS => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok((Protocol::Dns(Cow::Borrowed(str::from_utf8(data)?)), rest)) + } + DNS4 => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok((Protocol::Dns4(Cow::Borrowed(str::from_utf8(data)?)), rest)) + } + DNS6 => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok((Protocol::Dns6(Cow::Borrowed(str::from_utf8(data)?)), rest)) + } + DNSADDR => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok(( + Protocol::Dnsaddr(Cow::Borrowed(str::from_utf8(data)?)), + rest, + )) + } + HTTP => Ok((Protocol::Http, input)), + HTTPS => Ok((Protocol::Https, input)), + IP4 => { + let (data, rest) = split_at(4, input)?; + Ok(( + Protocol::Ip4(Ipv4Addr::new(data[0], data[1], data[2], data[3])), + rest, + )) + } + IP6 => { + let (data, rest) = split_at(16, input)?; + let mut rdr = Cursor::new(data); + let mut seg = [0_u16; 8]; + + for x in seg.iter_mut() { + *x = rdr.read_u16::()?; + } + + let addr = Ipv6Addr::new( + seg[0], seg[1], seg[2], seg[3], seg[4], seg[5], seg[6], seg[7], + ); + + Ok((Protocol::Ip6(addr), rest)) + } + P2P_WEBRTC_DIRECT => Ok((Protocol::P2pWebRtcDirect, input)), + P2P_WEBRTC_STAR => Ok((Protocol::P2pWebRtcStar, input)), + WEBRTC_DIRECT => Ok((Protocol::WebRTCDirect, input)), + CERTHASH => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok((Protocol::Certhash(Multihash::from_bytes(data)?), rest)) + } + P2P_WEBSOCKET_STAR => Ok((Protocol::P2pWebSocketStar, input)), + MEMORY => { + let (data, rest) = split_at(8, input)?; + let mut rdr = Cursor::new(data); + let num = rdr.read_u64::()?; + Ok((Protocol::Memory(num), rest)) + } + ONION => { + let (data, rest) = split_at(12, input)?; + let port = BigEndian::read_u16(&data[10..]); + Ok(( + Protocol::Onion(Cow::Borrowed(array_ref!(data, 0, 10)), port), + rest, + )) + } + ONION3 => { + let (data, rest) = split_at(37, input)?; + let port = BigEndian::read_u16(&data[35..]); + Ok(( + Protocol::Onion3((array_ref!(data, 0, 35), port).into()), + rest, + )) + } + P2P => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok(( + Protocol::P2p( + PeerId::from_bytes(data).map_err(|e| Error::ParsingError(Box::new(e)))?, + ), + rest, + )) + } + P2P_CIRCUIT => Ok((Protocol::P2pCircuit, input)), + QUIC => Ok((Protocol::Quic, input)), + QUIC_V1 => Ok((Protocol::QuicV1, input)), + SCTP => { + let (data, rest) = split_at(2, input)?; + let mut rdr = Cursor::new(data); + let num = rdr.read_u16::()?; + Ok((Protocol::Sctp(num), rest)) + } + TCP => { + let (data, rest) = split_at(2, input)?; + let mut rdr = Cursor::new(data); + let num = rdr.read_u16::()?; + Ok((Protocol::Tcp(num), rest)) + } + TLS => Ok((Protocol::Tls, input)), + NOISE => Ok((Protocol::Noise, input)), + UDP => { + let (data, rest) = split_at(2, input)?; + let mut rdr = Cursor::new(data); + let num = rdr.read_u16::()?; + Ok((Protocol::Udp(num), rest)) + } + UDT => Ok((Protocol::Udt, input)), + UNIX => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok((Protocol::Unix(Cow::Borrowed(str::from_utf8(data)?)), rest)) + } + UTP => Ok((Protocol::Utp, input)), + WEBTRANSPORT => Ok((Protocol::WebTransport, input)), + WS => Ok((Protocol::Ws(Cow::Borrowed("/")), input)), + WS_WITH_PATH => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok((Protocol::Ws(Cow::Borrowed(str::from_utf8(data)?)), rest)) + } + WSS => Ok((Protocol::Wss(Cow::Borrowed("/")), input)), + WSS_WITH_PATH => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok((Protocol::Wss(Cow::Borrowed(str::from_utf8(data)?)), rest)) + } + IP6ZONE => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok(( + Protocol::Ip6zone(Cow::Borrowed(str::from_utf8(data)?)), + rest, + )) + } + IPCIDR => { + let (data, rest) = split_at(1, input)?; + Ok((Protocol::Ipcidr(data[0]), rest)) + } + GARLIC64 => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok((Protocol::Garlic64(Cow::Borrowed(data)), rest)) + } + GARLIC32 => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok((Protocol::Garlic32(Cow::Borrowed(data)), rest)) + } + SNI => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok((Protocol::Sni(Cow::Borrowed(str::from_utf8(data)?)), rest)) + } + P2P_STARDUST => Ok((Protocol::P2pStardust, input)), + WEBRTC => Ok((Protocol::WebRTC, input)), + HTTP_PATH => { + let (n, input) = decode::usize(input)?; + let (data, rest) = split_at(n, input)?; + Ok(( + Protocol::HttpPath(Cow::Borrowed(str::from_utf8(data)?)), + rest, + )) + } + _ => Err(Error::UnknownProtocolId(id)), + } + } + + /// Encode this protocol by writing its binary representation into + /// the given `Write` impl. + pub fn write_bytes(&self, w: &mut W) -> Result<()> { + let mut buf = encode::u32_buffer(); + match self { + Protocol::Ip4(addr) => { + w.write_all(encode::u32(IP4, &mut buf))?; + w.write_all(&addr.octets())? + } + Protocol::Ip6(addr) => { + w.write_all(encode::u32(IP6, &mut buf))?; + for &segment in &addr.segments() { + w.write_u16::(segment)? + } + } + Protocol::Tcp(port) => { + w.write_all(encode::u32(TCP, &mut buf))?; + w.write_u16::(*port)? + } + Protocol::Tls => w.write_all(encode::u32(TLS, &mut buf))?, + Protocol::Noise => w.write_all(encode::u32(NOISE, &mut buf))?, + Protocol::Udp(port) => { + w.write_all(encode::u32(UDP, &mut buf))?; + w.write_u16::(*port)? + } + Protocol::Dccp(port) => { + w.write_all(encode::u32(DCCP, &mut buf))?; + w.write_u16::(*port)? + } + Protocol::Sctp(port) => { + w.write_all(encode::u32(SCTP, &mut buf))?; + w.write_u16::(*port)? + } + Protocol::Dns(s) => { + w.write_all(encode::u32(DNS, &mut buf))?; + let bytes = s.as_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(bytes)? + } + Protocol::Dns4(s) => { + w.write_all(encode::u32(DNS4, &mut buf))?; + let bytes = s.as_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(bytes)? + } + Protocol::Dns6(s) => { + w.write_all(encode::u32(DNS6, &mut buf))?; + let bytes = s.as_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(bytes)? + } + Protocol::Dnsaddr(s) => { + w.write_all(encode::u32(DNSADDR, &mut buf))?; + let bytes = s.as_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(bytes)? + } + Protocol::Unix(s) => { + w.write_all(encode::u32(UNIX, &mut buf))?; + let bytes = s.as_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(bytes)? + } + Protocol::P2p(peer_id) => { + w.write_all(encode::u32(P2P, &mut buf))?; + let bytes = peer_id.to_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(&bytes)? + } + Protocol::Onion(addr, port) => { + w.write_all(encode::u32(ONION, &mut buf))?; + w.write_all(addr.as_ref())?; + w.write_u16::(*port)? + } + Protocol::Onion3(addr) => { + w.write_all(encode::u32(ONION3, &mut buf))?; + w.write_all(addr.hash().as_ref())?; + w.write_u16::(addr.port())? + } + Protocol::Quic => w.write_all(encode::u32(QUIC, &mut buf))?, + Protocol::QuicV1 => w.write_all(encode::u32(QUIC_V1, &mut buf))?, + Protocol::Utp => w.write_all(encode::u32(UTP, &mut buf))?, + Protocol::Udt => w.write_all(encode::u32(UDT, &mut buf))?, + Protocol::Http => w.write_all(encode::u32(HTTP, &mut buf))?, + Protocol::Https => w.write_all(encode::u32(HTTPS, &mut buf))?, + Protocol::WebTransport => w.write_all(encode::u32(WEBTRANSPORT, &mut buf))?, + Protocol::Ws(ref s) if s == "/" => w.write_all(encode::u32(WS, &mut buf))?, + Protocol::Ws(s) => { + w.write_all(encode::u32(WS_WITH_PATH, &mut buf))?; + let bytes = s.as_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(bytes)? + } + Protocol::Wss(ref s) if s == "/" => w.write_all(encode::u32(WSS, &mut buf))?, + Protocol::Wss(s) => { + w.write_all(encode::u32(WSS_WITH_PATH, &mut buf))?; + let bytes = s.as_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(bytes)? + } + Protocol::P2pWebSocketStar => w.write_all(encode::u32(P2P_WEBSOCKET_STAR, &mut buf))?, + Protocol::P2pWebRtcStar => w.write_all(encode::u32(P2P_WEBRTC_STAR, &mut buf))?, + Protocol::WebRTCDirect => w.write_all(encode::u32(WEBRTC_DIRECT, &mut buf))?, + Protocol::Certhash(hash) => { + w.write_all(encode::u32(CERTHASH, &mut buf))?; + let bytes = hash.to_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(&bytes)? + } + Protocol::P2pWebRtcDirect => w.write_all(encode::u32(P2P_WEBRTC_DIRECT, &mut buf))?, + Protocol::P2pCircuit => w.write_all(encode::u32(P2P_CIRCUIT, &mut buf))?, + Protocol::Memory(port) => { + w.write_all(encode::u32(MEMORY, &mut buf))?; + w.write_u64::(*port)? + } + Protocol::Ip6zone(zone_id) => { + w.write_all(encode::u32(IP6ZONE, &mut buf))?; + let bytes = zone_id.as_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(bytes)? + } + Protocol::Ipcidr(mask) => { + w.write_all(encode::u32(IPCIDR, &mut buf))?; + w.write_u8(*mask)? + } + Protocol::Garlic64(addr) => { + w.write_all(encode::u32(GARLIC64, &mut buf))?; + w.write_all(encode::usize(addr.len(), &mut encode::usize_buffer()))?; + w.write_all(addr)? + } + Protocol::Garlic32(addr) => { + w.write_all(encode::u32(GARLIC32, &mut buf))?; + w.write_all(encode::usize(addr.len(), &mut encode::usize_buffer()))?; + w.write_all(addr)? + } + Protocol::Sni(s) => { + w.write_all(encode::u32(SNI, &mut buf))?; + let bytes = s.as_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(bytes)? + } + Protocol::P2pStardust => w.write_all(encode::u32(P2P_STARDUST, &mut buf))?, + Protocol::WebRTC => w.write_all(encode::u32(WEBRTC, &mut buf))?, + Protocol::HttpPath(s) => { + w.write_all(encode::u32(HTTP_PATH, &mut buf))?; + let bytes = s.as_bytes(); + w.write_all(encode::usize(bytes.len(), &mut encode::usize_buffer()))?; + w.write_all(bytes)? + } + } + Ok(()) + } + + /// Turn this `Protocol` into one that owns its data, thus being valid for any lifetime. + pub fn acquire<'b>(self) -> Protocol<'b> { + use self::Protocol::*; + match self { + Dccp(a) => Dccp(a), + Dns(cow) => Dns(Cow::Owned(cow.into_owned())), + Dns4(cow) => Dns4(Cow::Owned(cow.into_owned())), + Dns6(cow) => Dns6(Cow::Owned(cow.into_owned())), + Dnsaddr(cow) => Dnsaddr(Cow::Owned(cow.into_owned())), + Http => Http, + Https => Https, + Ip4(a) => Ip4(a), + Ip6(a) => Ip6(a), + P2pWebRtcDirect => P2pWebRtcDirect, + P2pWebRtcStar => P2pWebRtcStar, + WebRTCDirect => WebRTCDirect, + Certhash(hash) => Certhash(hash), + P2pWebSocketStar => P2pWebSocketStar, + Memory(a) => Memory(a), + Onion(addr, port) => Onion(Cow::Owned(addr.into_owned()), port), + Onion3(addr) => Onion3(addr.acquire()), + P2p(a) => P2p(a), + P2pCircuit => P2pCircuit, + Quic => Quic, + QuicV1 => QuicV1, + Sctp(a) => Sctp(a), + Tcp(a) => Tcp(a), + Tls => Tls, + Noise => Noise, + Udp(a) => Udp(a), + Udt => Udt, + Unix(cow) => Unix(Cow::Owned(cow.into_owned())), + Utp => Utp, + WebTransport => WebTransport, + Ws(cow) => Ws(Cow::Owned(cow.into_owned())), + Wss(cow) => Wss(Cow::Owned(cow.into_owned())), + Ip6zone(cow) => Ip6zone(Cow::Owned(cow.into_owned())), + Ipcidr(mask) => Ipcidr(mask), + Garlic64(addr) => Garlic64(Cow::Owned(addr.into_owned())), + Garlic32(addr) => Garlic32(Cow::Owned(addr.into_owned())), + Sni(cow) => Sni(Cow::Owned(cow.into_owned())), + P2pStardust => P2pStardust, + WebRTC => WebRTC, + HttpPath(cow) => HttpPath(Cow::Owned(cow.into_owned())), + } + } + + pub fn tag(&self) -> &'static str { + use self::Protocol::*; + match self { + Dccp(_) => "dccp", + Dns(_) => "dns", + Dns4(_) => "dns4", + Dns6(_) => "dns6", + Dnsaddr(_) => "dnsaddr", + Http => "http", + Https => "https", + Ip4(_) => "ip4", + Ip6(_) => "ip6", + P2pWebRtcDirect => "p2p-webrtc-direct", + P2pWebRtcStar => "p2p-webrtc-star", + WebRTCDirect => "webrtc-direct", + Certhash(_) => "certhash", + P2pWebSocketStar => "p2p-websocket-star", + Memory(_) => "memory", + Onion(_, _) => "onion", + Onion3(_) => "onion3", + P2p(_) => "p2p", + P2pCircuit => "p2p-circuit", + Quic => "quic", + QuicV1 => "quic-v1", + Sctp(_) => "sctp", + Tcp(_) => "tcp", + Tls => "tls", + Noise => "noise", + Udp(_) => "udp", + Udt => "udt", + Unix(_) => "unix", + Utp => "utp", + WebTransport => "webtransport", + Ws(ref s) if s == "/" => "ws", + Ws(_) => "x-parity-ws", + Wss(ref s) if s == "/" => "wss", + Wss(_) => "x-parity-wss", + Ip6zone(_) => "ip6zone", + Ipcidr(_) => "ipcidr", + Garlic64(_) => "garlic64", + Garlic32(_) => "garlic32", + Sni(_) => "sni", + P2pStardust => "p2p-stardust", + WebRTC => "webrtc", + HttpPath(_) => "http-path", + } + } +} + +impl fmt::Display for Protocol<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use self::Protocol::*; + write!(f, "/{}", self.tag())?; + match self { + Dccp(port) => write!(f, "/{port}"), + Dns(s) => write!(f, "/{s}"), + Dns4(s) => write!(f, "/{s}"), + Dns6(s) => write!(f, "/{s}"), + Dnsaddr(s) => write!(f, "/{s}"), + Ip4(addr) => write!(f, "/{addr}"), + Ip6(addr) => write!(f, "/{addr}"), + Certhash(hash) => write!( + f, + "/{}", + multibase::encode(multibase::Base::Base64Url, hash.to_bytes()) + ), + Memory(port) => write!(f, "/{port}"), + Onion(addr, port) => { + let s = BASE32.encode(addr.as_ref()); + write!(f, "/{}:{}", s.to_lowercase(), port) + } + Onion3(addr) => { + let s = BASE32.encode(addr.hash()); + write!(f, "/{}:{}", s.to_lowercase(), addr.port()) + } + P2p(c) => write!(f, "/{}", multibase::Base::Base58Btc.encode(c.to_bytes())), + Sctp(port) => write!(f, "/{port}"), + Tcp(port) => write!(f, "/{port}"), + Udp(port) => write!(f, "/{port}"), + Unix(s) => write!(f, "/{s}"), + Ws(s) if s != "/" => { + let encoded = + percent_encoding::percent_encode(s.as_bytes(), PATH_SEGMENT_ENCODE_SET); + write!(f, "/{encoded}") + } + Wss(s) if s != "/" => { + let encoded = + percent_encoding::percent_encode(s.as_bytes(), PATH_SEGMENT_ENCODE_SET); + write!(f, "/{encoded}") + } + Ip6zone(zone) => write!(f, "/{zone}"), + Ipcidr(mask) => write!(f, "/{mask}"), + Garlic64(addr) => write!( + f, + "/{}", + multibase::Base::Base64 + .encode(addr) + .replace('+', "-") + .replace('/', "~") + ), + Garlic32(addr) => write!(f, "/{}", multibase::Base::Base32Lower.encode(addr)), + Sni(s) => write!(f, "/{s}"), + HttpPath(s) => { + let encoded = + percent_encoding::percent_encode(s.as_bytes(), PATH_SEGMENT_ENCODE_SET); + write!(f, "/{encoded}") + } + _ => Ok(()), + } + } +} + +impl From for Protocol<'_> { + #[inline] + fn from(addr: IpAddr) -> Self { + match addr { + IpAddr::V4(addr) => Protocol::Ip4(addr), + IpAddr::V6(addr) => Protocol::Ip6(addr), + } + } +} + +impl From for Protocol<'_> { + #[inline] + fn from(addr: Ipv4Addr) -> Self { + Protocol::Ip4(addr) + } +} + +impl From for Protocol<'_> { + #[inline] + fn from(addr: Ipv6Addr) -> Self { + Protocol::Ip6(addr) + } +} + +macro_rules! read_onion_impl { + ($name:ident, $len:expr, $encoded_len:expr) => { + fn $name(s: &str) -> Result<([u8; $len], u16)> { + let mut parts = s.split(':'); + + // address part (without ".onion") + let b32 = parts.next().ok_or(Error::InvalidMultiaddr)?; + if b32.len() != $encoded_len { + return Err(Error::InvalidMultiaddr); + } + + // port number + let port = parts + .next() + .ok_or(Error::InvalidMultiaddr) + .and_then(|p| str::parse(p).map_err(From::from))?; + + // port == 0 is not valid for onion + if port == 0 { + return Err(Error::InvalidMultiaddr); + } + + // nothing else expected + if parts.next().is_some() { + return Err(Error::InvalidMultiaddr); + } + + if $len + != BASE32 + .decode_len(b32.len()) + .map_err(|_| Error::InvalidMultiaddr)? + { + return Err(Error::InvalidMultiaddr); + } + + let mut buf = [0u8; $len]; + BASE32 + .decode_mut(b32.as_bytes(), &mut buf) + .map_err(|_| Error::InvalidMultiaddr)?; + + Ok((buf, port)) + } + }; +} + +// Parse a version 2 onion address and return its binary representation. +// +// Format: ":" +read_onion_impl!(read_onion, 10, 16); +// Parse a version 3 onion address and return its binary representation. +// +// Format: ":" +read_onion_impl!(read_onion3, 35, 56); diff --git a/multiaddr/tests/lib.rs b/multiaddr/tests/lib.rs new file mode 100644 index 00000000000..936809eeee4 --- /dev/null +++ b/multiaddr/tests/lib.rs @@ -0,0 +1,833 @@ +use data_encoding::HEXUPPER; +use multiaddr::*; +use multihash::Multihash; +use quickcheck::{Arbitrary, Gen, QuickCheck}; +use std::{ + borrow::Cow, + convert::{TryFrom, TryInto}, + iter::{self, FromIterator}, + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + +// Property tests + +#[test] +fn to_from_bytes_identity() { + fn prop(a: Ma) -> bool { + let b = a.0.to_vec(); + Some(a) == Multiaddr::try_from(b).ok().map(Ma) + } + QuickCheck::new().quickcheck(prop as fn(Ma) -> bool) +} + +#[test] +fn to_from_str_identity() { + fn prop(a: Ma) -> bool { + let b = a.0.to_string(); + Some(a) == Multiaddr::from_str(&b).ok().map(Ma) + } + QuickCheck::new().quickcheck(prop as fn(Ma) -> bool) +} + +#[test] +fn byteswriter() { + fn prop(a: Ma, b: Ma) -> bool { + let mut x = a.0.clone(); + for p in b.0.iter() { + x = x.with(p) + } + x.iter() + .zip(a.0.iter().chain(b.0.iter())) + .all(|(x, y)| x == y) + } + QuickCheck::new().quickcheck(prop as fn(Ma, Ma) -> bool) +} + +#[test] +fn push_pop_identity() { + fn prop(a: Ma, p: Proto) -> bool { + let mut b = a.clone(); + let q = p.clone(); + b.0.push(q.0); + assert_ne!(a.0, b.0); + Some(p.0) == b.0.pop() && a.0 == b.0 + } + QuickCheck::new().quickcheck(prop as fn(Ma, Proto) -> bool) +} + +#[test] +fn ends_with() { + fn prop(Ma(m): Ma) { + let n = m.iter().count(); + for i in 0..n { + let suffix = m.iter().skip(i).collect::(); + assert!(m.ends_with(&suffix)); + } + } + QuickCheck::new().quickcheck(prop as fn(_)) +} + +#[test] +fn starts_with() { + fn prop(Ma(m): Ma) { + let n = m.iter().count(); + for i in 0..n { + let prefix = m.iter().take(i + 1).collect::(); + assert!(m.starts_with(&prefix)); + } + } + QuickCheck::new().quickcheck(prop as fn(_)) +} + +// Arbitrary impls + +#[derive(PartialEq, Eq, Clone, Hash, Debug)] +struct Ma(Multiaddr); + +impl Arbitrary for Ma { + fn arbitrary(g: &mut Gen) -> Self { + let iter = (0..u8::arbitrary(g) % 128).map(|_| Proto::arbitrary(g).0); + Ma(Multiaddr::from_iter(iter)) + } +} + +#[derive(PartialEq, Eq, Clone, Debug)] +struct Proto(Protocol<'static>); + +impl Proto { + const IMPL_VARIANT_COUNT: u8 = 40; +} + +impl Arbitrary for Proto { + fn arbitrary(g: &mut Gen) -> Self { + use Protocol::*; + match u8::arbitrary(g) % Proto::IMPL_VARIANT_COUNT { + 0 => Proto(Dccp(Arbitrary::arbitrary(g))), + 1 => Proto(Dns(Cow::Owned(SubString::arbitrary(g).0))), + 2 => Proto(Dns4(Cow::Owned(SubString::arbitrary(g).0))), + 3 => Proto(Dns6(Cow::Owned(SubString::arbitrary(g).0))), + 4 => Proto(Dnsaddr(Cow::Owned(SubString::arbitrary(g).0))), + 5 => Proto(Http), + 6 => Proto(Https), + 7 => Proto(Ip4(Ipv4Addr::arbitrary(g))), + 8 => Proto(Ip6(Ipv6Addr::arbitrary(g))), + 9 => Proto(P2pWebRtcDirect), + 10 => Proto(P2pWebRtcStar), + 11 => Proto(WebRTCDirect), + 12 => Proto(Certhash(Mh::arbitrary(g).0)), + 13 => Proto(P2pWebSocketStar), + 14 => Proto(Memory(Arbitrary::arbitrary(g))), + 15 => { + let a = iter::repeat_with(|| u8::arbitrary(g)) + .take(10) + .collect::>() + .try_into() + .unwrap(); + Proto(Onion(Cow::Owned(a), std::cmp::max(1, u16::arbitrary(g)))) + } + 16 => { + let a: [u8; 35] = iter::repeat_with(|| u8::arbitrary(g)) + .take(35) + .collect::>() + .try_into() + .unwrap(); + Proto(Onion3((a, std::cmp::max(1, u16::arbitrary(g))).into())) + } + 17 => Proto(P2p(PId::arbitrary(g).0)), + 18 => Proto(P2pCircuit), + 19 => Proto(Quic), + 20 => Proto(QuicV1), + 21 => Proto(Sctp(Arbitrary::arbitrary(g))), + 22 => Proto(Tcp(Arbitrary::arbitrary(g))), + 23 => Proto(Tls), + 24 => Proto(Noise), + 25 => Proto(Udp(Arbitrary::arbitrary(g))), + 26 => Proto(Udt), + 27 => Proto(Unix(Cow::Owned(SubString::arbitrary(g).0))), + 28 => Proto(Utp), + 29 => Proto(WebTransport), + 30 => Proto(Ws("/".into())), + 31 => Proto(Wss("/".into())), + 32 => Proto(Ip6zone(Cow::Owned(SubString::arbitrary(g).0))), + 33 => Proto(Ipcidr(Arbitrary::arbitrary(g))), + 34 => { + let len = usize::arbitrary(g) % (462 - 387) + 387; + let a = iter::repeat_with(|| u8::arbitrary(g)) + .take(len) + .collect::>(); + Proto(Garlic64(Cow::Owned(a))) + } + 35 => { + let len = if bool::arbitrary(g) { + 32 + } else { + usize::arbitrary(g) % 128 + 35 + }; + let a = iter::repeat_with(|| u8::arbitrary(g)) + .take(len) + .collect::>(); + Proto(Garlic32(Cow::Owned(a))) + } + 36 => Proto(Sni(Cow::Owned(SubString::arbitrary(g).0))), + 37 => Proto(P2pStardust), + 38 => Proto(WebRTC), + 39 => Proto(HttpPath(Cow::Owned(SubString::arbitrary(g).0))), + _ => panic!("outside range"), + } + } +} + +#[derive(Clone, Debug)] +struct Mh(Multihash<64>); + +impl Arbitrary for Mh { + fn arbitrary(g: &mut Gen) -> Self { + let mut hash: [u8; 32] = [0; 32]; + hash.fill_with(|| u8::arbitrary(g)); + Mh(Multihash::wrap(0x0, &hash).expect("The digest size is never too large")) + } +} + +#[derive(Clone, Debug)] +struct PId(PeerId); + +impl Arbitrary for PId { + fn arbitrary(g: &mut Gen) -> Self { + let mh = Mh::arbitrary(g); + + PId(PeerId::from_multihash(mh.0).expect("identity multihash works if digest size < 64")) + } +} + +#[derive(PartialEq, Eq, Clone, Debug)] +struct SubString(String); // ASCII string without '/' + +impl Arbitrary for SubString { + fn arbitrary(g: &mut Gen) -> Self { + let mut s = String::arbitrary(g); + s.retain(|c| c.is_ascii() && c != '/'); + SubString(s) + } +} + +// other unit tests + +fn ma_valid(source: &str, target: &str, protocols: Vec>) { + let parsed = source.parse::().unwrap(); + assert_eq!(HEXUPPER.encode(&parsed.to_vec()[..]), target); + assert_eq!(parsed.iter().collect::>(), protocols); + assert_eq!(source.parse::().unwrap().to_string(), source); + assert_eq!( + Multiaddr::try_from(HEXUPPER.decode(target.as_bytes()).unwrap()).unwrap(), + parsed + ); +} + +fn peer_id(s: &str) -> PeerId { + s.parse().unwrap() +} + +#[test] +fn multiaddr_eq() { + let m1 = "/ip4/127.0.0.1/udp/1234".parse::().unwrap(); + let m2 = "/ip4/127.0.0.1/tcp/1234".parse::().unwrap(); + let m3 = "/ip4/127.0.0.1/tcp/1234".parse::().unwrap(); + + assert_ne!(m1, m2); + assert_ne!(m2, m1); + assert_eq!(m2, m3); + assert_eq!(m1, m1); +} + +#[test] +fn construct_success() { + use Protocol::*; + + let local: Ipv4Addr = "127.0.0.1".parse().unwrap(); + let addr6: Ipv6Addr = "2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095".parse().unwrap(); + + ma_valid( + "/ip4/1.2.3.4", + "0401020304", + vec![Ip4("1.2.3.4".parse().unwrap())], + ); + ma_valid( + "/ip4/0.0.0.0", + "0400000000", + vec![Ip4("0.0.0.0".parse().unwrap())], + ); + ma_valid( + "/ip6/::1", + "2900000000000000000000000000000001", + vec![Ip6("::1".parse().unwrap())], + ); + ma_valid( + "/ip6/2601:9:4f81:9700:803e:ca65:66e8:c21", + "29260100094F819700803ECA6566E80C21", + vec![Ip6("2601:9:4f81:9700:803e:ca65:66e8:c21".parse().unwrap())], + ); + ma_valid( + "/ip6/fe80::9700:803e:ca65:66e8:c21/ip6zone/wlan0", + "29FE80000000009700803ECA6566E80C212A05776C616E30", + vec![ + Ip6("fe80::9700:803e:ca65:66e8:c21".parse().unwrap()), + Ip6zone(Cow::Borrowed("wlan0")), + ], + ); + ma_valid("/udp/0", "91020000", vec![Udp(0)]); + ma_valid("/tcp/0", "060000", vec![Tcp(0)]); + ma_valid("/sctp/0", "84010000", vec![Sctp(0)]); + ma_valid("/udp/1234", "910204D2", vec![Udp(1234)]); + ma_valid("/tcp/1234", "0604D2", vec![Tcp(1234)]); + ma_valid("/sctp/1234", "840104D2", vec![Sctp(1234)]); + ma_valid("/udp/65535", "9102FFFF", vec![Udp(65535)]); + ma_valid("/tcp/65535", "06FFFF", vec![Tcp(65535)]); + ma_valid( + "/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "A503221220D52EBB89D85B02A284948203A62FF28389C57C9F42BEEC4EC20DB76A68911C0B", + vec![P2p(peer_id( + "QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + ))], + ); + ma_valid( + "/udp/1234/sctp/1234", + "910204D2840104D2", + vec![Udp(1234), Sctp(1234)], + ); + ma_valid("/udp/1234/udt", "910204D2AD02", vec![Udp(1234), Udt]); + ma_valid("/udp/1234/utp", "910204D2AE02", vec![Udp(1234), Utp]); + ma_valid("/tcp/1234/http", "0604D2E003", vec![Tcp(1234), Http]); + ma_valid( + "/tcp/1234/tls/http", + "0604D2C003E003", + vec![Tcp(1234), Tls, Http], + ); + ma_valid( + "/tcp/1234/http/http-path/user", + "0604D2E003E1030475736572", + vec![Tcp(1234), Http, HttpPath(Cow::Borrowed("user"))], + ); + ma_valid( + "/tcp/1234/http/http-path/api%2Fv0%2Flogin", + "0604D2E003E1030C6170692F76302F6C6F67696E", + vec![Tcp(1234), Http, HttpPath(Cow::Borrowed("api/v0/login"))], + ); + ma_valid( + "/tcp/1234/http/http-path/a%2520space", + "0604D2E003E10309612532307370616365", + vec![Tcp(1234), Http, HttpPath(Cow::Borrowed("a%20space"))], + ); + ma_valid("/tcp/1234/https", "0604D2BB03", vec![Tcp(1234), Https]); + ma_valid( + "/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234", + "A503221220D52EBB89D85B02A284948203A62FF28389C57C9F42BEEC4EC20DB76A68911C0B0604D2", + vec![ + P2p(peer_id("QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC")), + Tcp(1234), + ], + ); + ma_valid( + "/ip4/127.0.0.1/udp/1234", + "047F000001910204D2", + vec![Ip4(local), Udp(1234)], + ); + ma_valid( + "/ip4/127.0.0.1/udp/0", + "047F00000191020000", + vec![Ip4(local), Udp(0)], + ); + ma_valid( + "/ip4/127.0.0.1/tcp/1234", + "047F0000010604D2", + vec![Ip4(local), Tcp(1234)], + ); + ma_valid( + "/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "047F000001A503221220D52EBB89D85B02A284948203A62FF28389C57C9F42BEEC4EC20DB76A68911C0B", + vec![ + Ip4(local), + P2p(peer_id("QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC")), + ], + ); + ma_valid("/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234", + "047F000001A503221220D52EBB89D85B02A284948203A62FF28389C57C9F42BEEC4EC20DB76A68911C0B0604D2", + vec![Ip4(local), P2p(peer_id("QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC")), Tcp(1234)]); + // /unix/a/b/c/d/e, + // /unix/stdio, + // /ip4/1.2.3.4/tcp/80/unix/a/b/c/d/e/f, + // /ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234/unix/stdio + ma_valid("/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/ws/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "29200108A07AC542013AC986FFFE317095061F40DD03A503221220D52EBB89D85B02A284948203A62FF28389C57C9F42BEEC4EC20DB76A68911C0B", + vec![Ip6(addr6), Tcp(8000), Ws("/".into()), P2p(peer_id("QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC")) + ]); + ma_valid("/p2p-webrtc-star/ip4/127.0.0.1/tcp/9090/ws/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "9302047F000001062382DD03A503221220D52EBB89D85B02A284948203A62FF28389C57C9F42BEEC4EC20DB76A68911C0B", + vec![P2pWebRtcStar, Ip4(local), Tcp(9090), Ws("/".into()), P2p(peer_id("QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC")) + ]); + ma_valid("/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/wss/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "29200108A07AC542013AC986FFFE317095061F40DE03A503221220D52EBB89D85B02A284948203A62FF28389C57C9F42BEEC4EC20DB76A68911C0B", + vec![Ip6(addr6), Tcp(8000), Wss("/".into()), P2p(peer_id("QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"))]); + ma_valid("/ip4/127.0.0.1/tcp/9090/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "047F000001062382A202A503221220D52EBB89D85B02A284948203A62FF28389C57C9F42BEEC4EC20DB76A68911C0B", + vec![Ip4(local), Tcp(9090), P2pCircuit, P2p(peer_id("QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"))]); + + ma_valid( + "/onion/aaimaq4ygg2iegci:80", + "BC030010C0439831B48218480050", + vec![Onion( + Cow::Owned([0, 16, 192, 67, 152, 49, 180, 130, 24, 72]), + 80, + )], + ); + ma_valid( + "/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:1234", + "BD03ADADEC040BE047F9658668B11A504F3155001F231A37F54C4476C07FB4CC139ED7E30304D2", + vec![Onion3( + ( + [ + 173, 173, 236, 4, 11, 224, 71, 249, 101, 134, 104, 177, 26, 80, 79, 49, 85, 0, + 31, 35, 26, 55, 245, 76, 68, 118, 192, 127, 180, 204, 19, 158, 215, 227, 3, + ], + 1234, + ) + .into(), + )], + ); + ma_valid( + "/garlic64/jT~IyXaoauTni6N4517EG8mrFUKpy0IlgZh-EY9csMAk82Odatmzr~YTZy8Hv7u~wvkg75EFNOyqb~nAPg-khyp2TS~ObUz8WlqYAM2VlEzJ7wJB91P-cUlKF\ + 18zSzVoJFmsrcQHZCirSbWoOknS6iNmsGRh5KVZsBEfp1Dg3gwTipTRIx7Vl5Vy~1OSKQVjYiGZS9q8RL0MF~7xFiKxZDLbPxk0AK9TzGGqm~wMTI2HS0Gm4Ycy8LYPVmLvG\ + onIBYndg2bJC7WLuF6tVjVquiokSVDKFwq70BCUU5AU-EvdOD5KEOAM7mPfw-gJUG4tm1TtvcobrObqoRnmhXPTBTN5H7qDD12AvlwFGnfAlBXjuP4xOUAISL5SRLiulrsMS\ + iT4GcugSI80mF6sdB0zWRgL1yyvoVWeTBn1TqjO27alr95DGTluuSqrNAxgpQzCKEWAyzrQkBfo2avGAmmz2NaHaAvYbOg0QSJz1PLjv2jdPW~ofiQmrGWM1cd~1cCqAAAA", + "BE0383038D3FC8C976A86AE4E78BA378E75EC41BC9AB1542A9CB422581987E118F5CB0C024F3639D6AD9B3AFF613672F07BFBBBFC2F920EF910534ECAA6FF9C03E\ + 0FA4872A764D2FCE6D4CFC5A5A9800CD95944CC9EF0241F753FE71494A175F334B35682459ACADC4076428AB49B5A83A49D2EA2366B06461E4A559B0111FA750E0D\ + E0C138A94D1231ED5979572FF53922905636221994BDABC44BD0C17FEF11622B16432DB3F193400AF53CC61AA9BFC0C4C8D874B41A6E18732F0B60F5662EF1A89C8\ + 0589DD8366C90BB58BB85EAD56356ABA2A244950CA170ABBD01094539014F84BDD383E4A10E00CEE63DFC3E809506E2D9B54EDBDCA1BACE6EAA119E68573D305337\ + 91FBA830F5D80BE5C051A77C09415E3B8FE3139400848BE5244B8AE96BB0C4A24F819CBA0488F34985EAC741D3359180BD72CAFA1559E4C19F54EA8CEDBB6A5AFDE\ + 4319396EB92AAB340C60A50CC2284580CB3AD09017E8D9ABC60269B3D8D687680BD86CE834412273D4F2E3BF68DD3D6FE87E2426AC658CD5C77FD5C0AA000000", + vec![Garlic64( + ( + &[ + 141, 63, 200, 201, 118, 168, 106, 228, 231, 139, 163, 120, 231, 94, 196, 27, 201, 171, 21, 66, + 169, 203, 66, 37, 129, 152, 126, 17, 143, 92, 176, 192, 36, 243, 99, 157, 106, 217, 179, 175, + 246, 19, 103, 47, 7, 191, 187, 191, 194, 249, 32, 239, 145, 5, 52, 236, 170, 111, 249, 192, + 62, 15, 164, 135, 42, 118, 77, 47, 206, 109, 76, 252, 90, 90, 152, 0, 205, 149, 148, 76, + 201, 239, 2, 65, 247, 83, 254, 113, 73, 74, 23, 95, 51, 75, 53, 104, 36, 89, 172, 173, + 196, 7, 100, 40, 171, 73, 181, 168, 58, 73, 210, 234, 35, 102, 176, 100, 97, 228, 165, 89, + 176, 17, 31, 167, 80, 224, 222, 12, 19, 138, 148, 209, 35, 30, 213, 151, 149, 114, 255, 83, + 146, 41, 5, 99, 98, 33, 153, 75, 218, 188, 68, 189, 12, 23, 254, 241, 22, 34, 177, 100, + 50, 219, 63, 25, 52, 0, 175, 83, 204, 97, 170, 155, 252, 12, 76, 141, 135, 75, 65, 166, + 225, 135, 50, 240, 182, 15, 86, 98, 239, 26, 137, 200, 5, 137, 221, 131, 102, 201, 11, 181, + 139, 184, 94, 173, 86, 53, 106, 186, 42, 36, 73, 80, 202, 23, 10, 187, 208, 16, 148, 83, + 144, 20, 248, 75, 221, 56, 62, 74, 16, 224, 12, 238, 99, 223, 195, 232, 9, 80, 110, 45, + 155, 84, 237, 189, 202, 27, 172, 230, 234, 161, 25, 230, 133, 115, 211, 5, 51, 121, 31, 186, + 131, 15, 93, 128, 190, 92, 5, 26, 119, 192, 148, 21, 227, 184, 254, 49, 57, 64, 8, 72, + 190, 82, 68, 184, 174, 150, 187, 12, 74, 36, 248, 25, 203, 160, 72, 143, 52, 152, 94, 172, + 116, 29, 51, 89, 24, 11, 215, 44, 175, 161, 85, 158, 76, 25, 245, 78, 168, 206, 219, 182, + 165, 175, 222, 67, 25, 57, 110, 185, 42, 171, 52, 12, 96, 165, 12, 194, 40, 69, 128, 203, + 58, 208, 144, 23, 232, 217, 171, 198, 2, 105, 179, 216, 214, 135, 104, 11, 216, 108, 232, 52, + 65, 34, 115, 212, 242, 227, 191, 104, 221, 61, 111, 232, 126, 36, 38, 172, 101, 140, 213, 199, + 127, 213, 192, 170, 0, 0, 0, + ] + ).into() + )], + ); + ma_valid( + "/dnsaddr/sjc-1.bootstrap.libp2p.io", + "3819736A632D312E626F6F7473747261702E6C69627032702E696F", + vec![Dnsaddr(Cow::Borrowed("sjc-1.bootstrap.libp2p.io"))], + ); + ma_valid( + "/dnsaddr/sjc-1.bootstrap.libp2p.io/tcp/1234/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "3819736A632D312E626F6F7473747261702E6C69627032702E696F0604D2A50322122006B3608AA000274049EB28AD8E793A26FF6FAB281A7D3BD77CD18EB745DFAABB", + vec![Dnsaddr(Cow::Borrowed("sjc-1.bootstrap.libp2p.io")), Tcp(1234), P2p(peer_id("QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN"))] + ); + ma_valid( + "/ip4/127.0.0.1/tcp/127/ws", + "047F00000106007FDD03", + vec![Ip4(local), Tcp(127), Ws("/".into())], + ); + ma_valid( + "/ip4/127.0.0.1/tcp/127/tls", + "047F00000106007FC003", + vec![Ip4(local), Tcp(127), Tls], + ); + ma_valid( + "/ip4/127.0.0.1/tcp/127/tls/ws", + "047F00000106007FC003DD03", + vec![Ip4(local), Tcp(127), Tls, Ws("/".into())], + ); + + ma_valid( + "/ip4/127.0.0.1/tcp/127/noise", + "047F00000106007FC603", + vec![Ip4(local), Tcp(127), Noise], + ); + + ma_valid( + "/ip4/127.0.0.1/udp/1234/webrtc-direct", + "047F000001910204D29802", + vec![Ip4(local), Udp(1234), WebRTCDirect], + ); + + let (_base, decoded) = + multibase::decode("uEiDDq4_xNyDorZBH3TlGazyJdOWSwvo4PUo5YHFMrvDE8g").unwrap(); + ma_valid( + "/ip4/127.0.0.1/udp/1234/webrtc-direct/certhash/uEiDDq4_xNyDorZBH3TlGazyJdOWSwvo4PUo5YHFMrvDE8g", + "047F000001910204D29802D203221220C3AB8FF13720E8AD9047DD39466B3C8974E592C2FA383D4A3960714CAEF0C4F2", + vec![ + Ip4(local), + Udp(1234), + WebRTCDirect, + Certhash(Multihash::from_bytes(&decoded).unwrap()), + ], + ); + + ma_valid( + "/ip4/127.0.0.1/udp/1234/quic/webtransport", + "047F000001910204D2CC03D103", + vec![Ip4(local), Udp(1234), Quic, WebTransport], + ); + + let (_base, decoded) = + multibase::decode("uEiDDq4_xNyDorZBH3TlGazyJdOWSwvo4PUo5YHFMrvDE8g").unwrap(); + ma_valid( + "/ip4/127.0.0.1/udp/1234/webtransport/certhash/uEiDDq4_xNyDorZBH3TlGazyJdOWSwvo4PUo5YHFMrvDE8g", + "047F000001910204D2D103D203221220C3AB8FF13720E8AD9047DD39466B3C8974E592C2FA383D4A3960714CAEF0C4F2", + vec![ + Ip4(local), + Udp(1234), + WebTransport, + Certhash(Multihash::from_bytes(&decoded).unwrap()), + ], + ); +} + +#[test] +fn construct_fail() { + let addresses = [ + "/ip4", + "/ip4/::1", + "/ip4/fdpsofodsajfdoisa", + "/ip6", + "/ip6/fe80::9700:803e:ca65:66e8:c21/ip6zone", + "/udp", + "/tcp", + "/sctp", + "/udp/65536", + "/tcp/65536", + "/onion/9imaq4ygg2iegci7:80", + "/onion/aaimaq4ygg2iegci7:80", + "/onion/timaq4ygg2iegci7:0", + "/onion/timaq4ygg2iegci7:-1", + "/onion/timaq4ygg2iegci7", + "/onion/timaq4ygg2iegci@:666", + "/onion3/9ww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:80", + "/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd7:80", + "/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:0", + "/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd:-1", + "/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd", + "/onion3/vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyy@:666", + "/garlic64/jT~", + "/garlic32/566niximlxdzpanmn4qouucvua3k7neniwss47li5r6ugoertzu", + "/garlic32/566niximlxdzpanmn4qouucvua3k7neniwss47li5r6ugoertzu77", + "/garlic32/566niximlxdzpanmn4qouucvua3k7neniwss47li5r6ugoertzu:80", + "/garlic32/566niximlxdzpanmn4qouucvua3k7neniwss47li5r6ugoertzuq:-1", + "/garlic32/566niximlxdzpanmn4qouucvua3k7neniwss47li5r6ugoertzu@", + "/udp/1234/sctp", + "/udp/1234/udt/1234", + "/udp/1234/utp/1234", + "/ip4/127.0.0.1/udp/jfodsajfidosajfoidsa", + "/ip4/127.0.0.1/udp", + "/ip4/127.0.0.1/tcp/jfodsajfidosajfoidsa", + "/ip4/127.0.0.1/tcp", + "/ip4/127.0.0.1/p2p", + "/ip4/127.0.0.1/p2p/tcp", + "/p2p-circuit/50", + "/ip4/127.0.0.1/udp/1234/webrtc-direct/certhash", + "/ip4/127.0.0.1/udp/1234/webrtc-direct/certhash/b2uaraocy6yrdblb4sfptaddgimjmmp", // 1 character missing from certhash + "/tcp/1234/http/http-path/a/b", + ]; + + for address in &addresses { + assert!( + address.parse::().is_err(), + "{}", + address.to_string() + ); + } +} + +#[test] +fn to_multiaddr() { + assert_eq!( + Multiaddr::from(Ipv4Addr::new(127, 0, 0, 1)), + "/ip4/127.0.0.1".parse().unwrap() + ); + assert_eq!( + Multiaddr::from(Ipv6Addr::new( + 0x2601, 0x9, 0x4f81, 0x9700, 0x803e, 0xca65, 0x66e8, 0xc21 + )), + "/ip6/2601:9:4f81:9700:803e:ca65:66e8:c21".parse().unwrap() + ); + assert_eq!( + Multiaddr::try_from("/ip4/127.0.0.1/tcp/1234".to_string()).unwrap(), + "/ip4/127.0.0.1/tcp/1234".parse::().unwrap() + ); + assert_eq!( + Multiaddr::try_from("/ip6/2601:9:4f81:9700:803e:ca65:66e8:c21").unwrap(), + "/ip6/2601:9:4f81:9700:803e:ca65:66e8:c21" + .parse::() + .unwrap() + ); + assert_eq!( + Multiaddr::from(Ipv4Addr::new(127, 0, 0, 1)).with(Protocol::Tcp(1234)), + "/ip4/127.0.0.1/tcp/1234".parse::().unwrap() + ); + assert_eq!( + Multiaddr::from(Ipv6Addr::new( + 0x2601, 0x9, 0x4f81, 0x9700, 0x803e, 0xca65, 0x66e8, 0xc21 + )) + .with(Protocol::Tcp(1234)), + "/ip6/2601:9:4f81:9700:803e:ca65:66e8:c21/tcp/1234" + .parse::() + .unwrap() + ); +} + +#[test] +fn from_bytes_fail() { + let bytes = vec![1, 2, 3, 4]; + assert!(Multiaddr::try_from(bytes).is_err()); +} + +#[test] +fn ser_and_deser_json() { + let addr: Multiaddr = "/ip4/0.0.0.0/tcp/0/tls".parse::().unwrap(); + let serialized = serde_json::to_string(&addr).unwrap(); + assert_eq!(serialized, "\"/ip4/0.0.0.0/tcp/0/tls\""); + let deserialized: Multiaddr = serde_json::from_str(&serialized).unwrap(); + assert_eq!(addr, deserialized); +} + +#[test] +fn ser_and_deser_bincode() { + let addr: Multiaddr = "/ip4/0.0.0.0/tcp/0/tls".parse::().unwrap(); + let serialized = bincode::serialize(&addr).unwrap(); + // compact addressing + assert_eq!( + serialized, + vec![10, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 6, 0, 0, 192, 3] + ); + let deserialized: Multiaddr = bincode::deserialize(&serialized).unwrap(); + assert_eq!(addr, deserialized); +} + +#[test] +fn append() { + let mut a: Multiaddr = Protocol::Ip4(Ipv4Addr::new(1, 2, 3, 4)).into(); + a.push(Protocol::Tcp(80)); + a.push(Protocol::Http); + + let mut i = a.iter(); + assert_eq!(Some(Protocol::Ip4(Ipv4Addr::new(1, 2, 3, 4))), i.next()); + assert_eq!(Some(Protocol::Tcp(80)), i.next()); + assert_eq!(Some(Protocol::Http), i.next()); + assert_eq!(None, i.next()) +} + +fn replace_ip_addr(a: &Multiaddr, p: Protocol<'_>) -> Option { + a.replace(0, move |x| match x { + Protocol::Ip4(_) | Protocol::Ip6(_) => Some(p), + _ => None, + }) +} + +#[test] +fn replace_ip4_with_ip4() { + let server = multiaddr!(Ip4(Ipv4Addr::LOCALHOST), Tcp(10000u16)); + let result = replace_ip_addr(&server, Protocol::Ip4([80, 81, 82, 83].into())).unwrap(); + assert_eq!(result, multiaddr!(Ip4([80, 81, 82, 83]), Tcp(10000u16))) +} + +#[test] +fn replace_ip6_with_ip4() { + let server = multiaddr!(Ip6(Ipv6Addr::LOCALHOST), Tcp(10000u16)); + let result = replace_ip_addr(&server, Protocol::Ip4([80, 81, 82, 83].into())).unwrap(); + assert_eq!(result, multiaddr!(Ip4([80, 81, 82, 83]), Tcp(10000u16))) +} + +#[test] +fn replace_ip4_with_ip6() { + let server = multiaddr!(Ip4(Ipv4Addr::LOCALHOST), Tcp(10000u16)); + let result = replace_ip_addr(&server, "2001:db8::1".parse::().unwrap().into()); + assert_eq!( + result.unwrap(), + "/ip6/2001:db8::1/tcp/10000".parse::().unwrap() + ) +} + +#[test] +fn unknown_protocol_string() { + match "/unknown/1.2.3.4".parse::() { + Ok(_) => panic!("The UnknownProtocolString error should be caused"), + Err(e) => match e { + crate::Error::UnknownProtocolString(protocol) => { + assert_eq!(protocol, "unknown") + } + _ => panic!("The UnknownProtocolString error should be caused"), + }, + } +} + +#[test] +fn protocol_stack() { + let addresses = [ + "/ip4/0.0.0.0", + "/ip6/::1", + "/ip6/2601:9:4f81:9700:803e:ca65:66e8:c21", + "/ip6/fe80::9700:803e:ca65:66e8:c21/ip6zone/wlan0", + "/udp/0", + "/tcp/0", + "/sctp/0", + "/udp/1234", + "/tcp/1234", + "/sctp/1234", + "/udp/65535", + "/tcp/65535", + "/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/udp/1234/sctp/1234", + "/udp/1234/udt", + "/udp/1234/utp", + "/tcp/1234/http", + "/tcp/1234/tls/http", + "/tcp/1234/http/http-path/user", + "/tcp/1234/http/http-path/api%2Fv1%2Flogin", + "/tcp/1234/http/http-path/a%20space", + "/tcp/1234/https", + "/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234", + "/ip4/127.0.0.1/udp/1234", + "/ip4/127.0.0.1/udp/0", + "/ip4/127.0.0.1/tcp/1234", + "/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234", + "/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/ws/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/p2p-webrtc-star/ip4/127.0.0.1/tcp/9090/ws/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/tcp/8000/wss/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/ip4/127.0.0.1/tcp/9090/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + "/onion/aaimaq4ygg2iegci:80", + "/dnsaddr/sjc-1.bootstrap.libp2p.io", + "/dnsaddr/sjc-1.bootstrap.libp2p.io/tcp/1234/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "/ip4/127.0.0.1/tcp/127/ws", + "/ip4/127.0.0.1/tcp/127/tls", + "/ip4/127.0.0.1/tcp/127/tls/ws", + "/ip4/127.0.0.1/tcp/127/noise", + "/ip4/127.0.0.1/udp/1234/webrtc-direct", + ]; + let argless = std::collections::HashSet::from([ + "http", + "https", + "noise", + "p2p-circuit", + "p2p-webrtc-direct", + "p2p-webrtc-star", + "p2p-websocket-star", + "quic", + "quic-v1", + "tls", + "udt", + "utp", + "webrtc-direct", + "ws", + "wss", + ]); + for addr_str in addresses { + let ma = Multiaddr::from_str(addr_str).expect("These are supposed to be valid multiaddrs"); + let ps: Vec<&str> = ma.protocol_stack().collect(); + let mut toks: Vec<&str> = addr_str.split('/').collect(); + assert_eq!("", toks[0]); + toks.remove(0); + let mut i = 0; + while i < toks.len() { + let proto_tag = toks[i]; + i += 1; + if argless.contains(proto_tag) { + //skip + } else { + toks.remove(i); + } + } + assert_eq!(ps, toks); + } +} + +// Assert all `Protocol` variants are covered +// in its `Arbitrary` impl. +#[cfg(nightly)] +#[test] +fn arbitrary_impl_for_all_proto_variants() { + let variants = core::mem::variant_count::() as u8; + assert_eq!(variants, Proto::IMPL_VARIANT_COUNT); +} + +mod multiaddr_with_p2p { + use multiaddr::{Multiaddr, PeerId}; + + fn test_multiaddr_with_p2p( + multiaddr: &str, + peer: &str, + expected: std::result::Result<&str, &str>, + ) { + let peer = peer.parse::().unwrap(); + let expected = expected + .map(|a| a.parse::().unwrap()) + .map_err(|a| a.parse::().unwrap()); + + let mut multiaddr = multiaddr.parse::().unwrap(); + // Testing multiple time to validate idempotence. + for _ in 0..3 { + let result = multiaddr.with_p2p(peer); + assert_eq!(result, expected); + multiaddr = result.unwrap_or_else(|addr| addr); + } + } + + #[test] + fn empty_multiaddr() { + // Multiaddr is empty -> it should push and return Ok. + test_multiaddr_with_p2p( + "", + "QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + Ok("/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN"), + ) + } + #[test] + fn non_p2p_terminated() { + // Last protocol is not p2p -> it should push and return Ok. + test_multiaddr_with_p2p( + "/ip4/127.0.0.1", + "QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + Ok("/ip4/127.0.0.1/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN"), + ) + } + + #[test] + fn p2p_terminated_same_peer() { + // Last protocol is p2p and the contained peer matches the provided one -> it should do nothing and return Ok. + test_multiaddr_with_p2p( + "/ip4/127.0.0.1/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + Ok("/ip4/127.0.0.1/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN"), + ) + } + + #[test] + fn p2p_terminated_different_peer() { + // Last protocol is p2p but the contained peer does not match the provided one -> it should do nothing and return Err. + test_multiaddr_with_p2p( + "/ip4/127.0.0.1/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC", + Err("/ip4/127.0.0.1/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN"), + ) + } +} diff --git a/muxers/mplex/Cargo.toml b/muxers/mplex/Cargo.toml index 4eb0a027363..a16b94df59a 100644 --- a/muxers/mplex/Cargo.toml +++ b/muxers/mplex/Cargo.toml @@ -30,7 +30,7 @@ futures = { workspace = true } libp2p-identity = { workspace = true, features = ["rand"] } libp2p-muxer-test-harness = { path = "../test-harness" } libp2p-plaintext = { workspace = true } -libp2p-tcp = { workspace = true, features = ["async-io"] } +libp2p-tcp = { workspace = true, features = ["tokio"] } quickcheck = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/muxers/mplex/benches/split_send_size.rs b/muxers/mplex/benches/split_send_size.rs index 51f6a85dc66..3210a0d4410 100644 --- a/muxers/mplex/benches/split_send_size.rs +++ b/muxers/mplex/benches/split_send_size.rs @@ -186,7 +186,7 @@ fn tcp_transport(split_send_size: usize) -> BenchTransport { let mut mplex = mplex::Config::default(); mplex.set_split_send_size(split_send_size); - libp2p_tcp::async_io::Transport::new(libp2p_tcp::Config::default().nodelay(true)) + libp2p_tcp::tokio::Transport::new(libp2p_tcp::Config::default().nodelay(true)) .upgrade(upgrade::Version::V1) .authenticate(plaintext::Config::new( &identity::Keypair::generate_ed25519(), diff --git a/protocols/autonat/CHANGELOG.md b/protocols/autonat/CHANGELOG.md index 16319724432..1ef95957e92 100644 --- a/protocols/autonat/CHANGELOG.md +++ b/protocols/autonat/CHANGELOG.md @@ -1,8 +1,10 @@ -## 0.14.1 +## 0.15.0 - Fix infinity loop on wrong `nonce` when performing `dial_back`. See [PR 5848](https://github.com/libp2p/rust-libp2p/pull/5848). + + ## 0.14.0 - Verify that an incoming AutoNAT dial comes from a connected peer. See [PR 5597](https://github.com/libp2p/rust-libp2p/pull/5597). diff --git a/protocols/autonat/Cargo.toml b/protocols/autonat/Cargo.toml index 61a15eae3bc..9a2f227d412 100644 --- a/protocols/autonat/Cargo.toml +++ b/protocols/autonat/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-autonat" edition.workspace = true rust-version = { workspace = true } description = "NAT and firewall detection for libp2p" -version = "0.14.1" +version = "0.15.0" authors = [ "David Craven ", "Elena Frank ", diff --git a/protocols/autonat/src/v1/behaviour.rs b/protocols/autonat/src/v1/behaviour.rs index 24ec1b13be7..9ac7cd01017 100644 --- a/protocols/autonat/src/v1/behaviour.rs +++ b/protocols/autonat/src/v1/behaviour.rs @@ -291,7 +291,7 @@ impl Behaviour { self.as_client().on_new_address(); } - fn as_client(&mut self) -> AsClient { + fn as_client(&mut self) -> AsClient<'_> { AsClient { inner: &mut self.inner, local_peer_id: self.local_peer_id, @@ -310,7 +310,7 @@ impl Behaviour { } } - fn as_server(&mut self) -> AsServer { + fn as_server(&mut self) -> AsServer<'_> { AsServer { inner: &mut self.inner, config: &self.config, diff --git a/protocols/dcutr/CHANGELOG.md b/protocols/dcutr/CHANGELOG.md index 61d6d0fff3a..f17b53ef902 100644 --- a/protocols/dcutr/CHANGELOG.md +++ b/protocols/dcutr/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.14.0 + + + ## 0.13.0 - Deprecate `void` crate. diff --git a/protocols/dcutr/Cargo.toml b/protocols/dcutr/Cargo.toml index 081bec8b22a..097b0dc1d51 100644 --- a/protocols/dcutr/Cargo.toml +++ b/protocols/dcutr/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-dcutr" edition.workspace = true rust-version = { workspace = true } description = "Direct connection upgrade through relay" -version = "0.13.0" +version = "0.14.0" authors = ["Max Inden "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" @@ -23,7 +23,7 @@ quick-protobuf = "0.8" quick-protobuf-codec = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } -lru = "0.12.3" +hashlink = { workspace = true } futures-bounded = { workspace = true } [dev-dependencies] @@ -32,7 +32,7 @@ libp2p-plaintext = { workspace = true } libp2p-relay = { workspace = true } libp2p-swarm = { workspace = true, features = ["macros"] } libp2p-swarm-test = { path = "../../swarm-test" } -libp2p-tcp = { workspace = true, features = ["async-io"] } +libp2p-tcp = { workspace = true, features = ["tokio"] } libp2p-yamux = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/protocols/dcutr/src/behaviour.rs b/protocols/dcutr/src/behaviour.rs index 4985527aca2..cb2fd654d4c 100644 --- a/protocols/dcutr/src/behaviour.rs +++ b/protocols/dcutr/src/behaviour.rs @@ -23,11 +23,11 @@ use std::{ collections::{HashMap, HashSet, VecDeque}, convert::Infallible, - num::NonZeroUsize, task::{Context, Poll}, }; use either::Either; +use hashlink::LruCache; use libp2p_core::{ connection::ConnectedPoint, multiaddr::Protocol, transport::PortUse, Endpoint, Multiaddr, }; @@ -38,7 +38,6 @@ use libp2p_swarm::{ dummy, ConnectionDenied, ConnectionHandler, ConnectionId, NetworkBehaviour, NewExternalAddrCandidate, NotifyHandler, THandler, THandlerInEvent, THandlerOutEvent, ToSwarm, }; -use lru::LruCache; use thiserror::Error; use crate::{handler, protocol}; @@ -361,7 +360,7 @@ struct Candidates { impl Candidates { fn new(me: PeerId) -> Self { Self { - inner: LruCache::new(NonZeroUsize::new(20).expect("20 > 0")), + inner: LruCache::new(20), me, } } @@ -375,7 +374,7 @@ impl Candidates { address.push(Protocol::P2p(self.me)); } - self.inner.push(address, ()); + self.inner.insert(address, ()); } fn iter(&self) -> impl Iterator { diff --git a/protocols/dcutr/src/handler/relayed.rs b/protocols/dcutr/src/handler/relayed.rs index af84d8fe9b1..43e433a5268 100644 --- a/protocols/dcutr/src/handler/relayed.rs +++ b/protocols/dcutr/src/handler/relayed.rs @@ -148,12 +148,8 @@ impl Handler { fn on_listen_upgrade_error( &mut self, - ListenUpgradeError { error, .. }: ListenUpgradeError< - (), - ::InboundProtocol, - >, + _: ListenUpgradeError<(), ::InboundProtocol>, ) { - libp2p_core::util::unreachable(error.into_inner()); } fn on_dial_upgrade_error( diff --git a/protocols/dcutr/tests/lib.rs b/protocols/dcutr/tests/lib.rs index d541ddb4fa9..b74a0e41ce0 100644 --- a/protocols/dcutr/tests/lib.rs +++ b/protocols/dcutr/tests/lib.rs @@ -135,7 +135,7 @@ fn build_client() -> Swarm { let transport = relay_transport .or_transport(MemoryTransport::default()) - .or_transport(libp2p_tcp::async_io::Transport::default()) + .or_transport(libp2p_tcp::tokio::Transport::default()) .upgrade(Version::V1) .authenticate(plaintext::Config::new(&local_key)) .multiplex(libp2p_yamux::Config::default()) diff --git a/protocols/floodsub/CHANGELOG.md b/protocols/floodsub/CHANGELOG.md index e2bd50141d7..1a586f1c46a 100644 --- a/protocols/floodsub/CHANGELOG.md +++ b/protocols/floodsub/CHANGELOG.md @@ -1,7 +1,10 @@ -## 0.46.1 +## 0.47.0 + - Rename types to match naming convention in [discussion 2174](https://github.com/libp2p/rust-libp2p/discussions/2174). See [PR 5855](https://github.com/libp2p/rust-libp2p/pull/5855). + + ## 0.46.0 diff --git a/protocols/floodsub/Cargo.toml b/protocols/floodsub/Cargo.toml index c9a14fa8cbc..8d0337246a4 100644 --- a/protocols/floodsub/Cargo.toml +++ b/protocols/floodsub/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-floodsub" edition.workspace = true rust-version = { workspace = true } description = "Floodsub protocol for libp2p" -version = "0.46.1" +version = "0.47.0" authors = ["Parity Technologies "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" diff --git a/protocols/gossipsub/CHANGELOG.md b/protocols/gossipsub/CHANGELOG.md index 9f2d0ad5f72..e6247855595 100644 --- a/protocols/gossipsub/CHANGELOG.md +++ b/protocols/gossipsub/CHANGELOG.md @@ -1,6 +1,64 @@ +## 0.50.0 +- Log when sending and receiving messages. + See [PR 6234](https://github.com/libp2p/rust-libp2p/pull/6234) + +- Prevent mesh exceeding mesh_n_high. + See [PR 6184](https://github.com/libp2p/rust-libp2p/pull/6184) + +- Fix underflow when shuffling peers after prunning. + See [PR 6183](https://github.com/libp2p/rust-libp2p/pull/6183) + +- Remove peer penalty for duplicate messages. + See [PR 6112](https://github.com/libp2p/rust-libp2p/pull/6112) + +- Remove `Rpc` from the public API. + See [PR 6091](https://github.com/libp2p/rust-libp2p/pull/6091) + +- reduce allocations by replacing `or_insert` with `or_insert_with` + See [PR 6136](https://github.com/libp2p/rust-libp2p/pull/6136) + +- Fix `unsubscribe_backoff` expecting number of seconds instead of `Duration` + See [PR 6124](https://github.com/libp2p/rust-libp2p/pull/6124) + +- Fix incorrect default values in ConfigBuilder + See [PR 6113](https://github.com/libp2p/rust-libp2p/pull/6113) + +- Remove duplicated config `set_topic_max_transmit_size` method, prefer `max_transmit_size_for_topic`. + See [PR 6173](https://github.com/libp2p/rust-libp2p/pull/6173). + +- Switch the internal `async-channel` used to dispatch messages from `NetworkBehaviour` to the `ConnectionHandler` + with an internal priority queue. See [PR 6175](https://github.com/libp2p/rust-libp2p/pull/6175) + +- gossipsub: do early return in for an empty input + See [PR 6208](https://github.com/libp2p/rust-libp2p/pull/6208). + +- Refactor gossipsub with in-place negative-score peer removal. + See [PR 6209](https://github.com/libp2p/rust-libp2p/pull/6209). + +- Avoid direct casting from u128 to u64. + See [PR 6211](https://github.com/libp2p/rust-libp2p/pull/6211). + +## 0.49.2 + +- Relax `Behaviour::with_metrics` requirements, do not require DataTransform and TopicSubscriptionFilter to also impl Default + See [PR 6097](https://github.com/libp2p/rust-libp2p/pull/6097) + +## 0.49.1 + +- Fix applying P3 and P6 Score penalties when their weight is zero + See [PR 6097](https://github.com/libp2p/rust-libp2p/pull/6097) + +- Fix fanout logic to include correctly scored peers and prevent panics when memcache is set to 0. + See [PR 6095](https://github.com/libp2p/rust-libp2p/pull/6095) + +- Fix mesh not being constructed even when not adding any peer. + See [PR 6100](https://github.com/libp2p/rust-libp2p/pull/6100) + ## 0.49.0 + - Feature gate metrics related code. This changes some `Behaviour` constructor methods. See [PR 6020](https://github.com/libp2p/rust-libp2p/pull/6020) + - Send IDONTWANT before Publishing a new message. See [PR 6017](https://github.com/libp2p/rust-libp2p/pull/6017) @@ -38,6 +96,8 @@ - Allow customizing max transmit size and mesh-n-* parameters per topic. See [PR 5868](https://github.com/libp2p/rust-libp2p/pull/5868) + + ## 0.48.0 - Allow broadcasting `IDONTWANT` messages when publishing to avoid downloading data that is already available. diff --git a/protocols/gossipsub/Cargo.toml b/protocols/gossipsub/Cargo.toml index bd706175890..911b453f477 100644 --- a/protocols/gossipsub/Cargo.toml +++ b/protocols/gossipsub/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-gossipsub" edition.workspace = true rust-version = { workspace = true } description = "Gossipsub protocol for libp2p" -version = "0.49.0" +version = "0.50.0" authors = ["Age Manning "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" diff --git a/protocols/gossipsub/src/behaviour.rs b/protocols/gossipsub/src/behaviour.rs index 45c84a39ffd..57e4b8edab8 100644 --- a/protocols/gossipsub/src/behaviour.rs +++ b/protocols/gossipsub/src/behaviour.rs @@ -61,7 +61,7 @@ use crate::{ mcache::MessageCache, peer_score::{PeerScore, PeerScoreParams, PeerScoreState, PeerScoreThresholds, RejectReason}, protocol::SIGNING_PREFIX, - rpc::Sender, + queue::Queue, rpc_proto::proto, subscription_filter::{AllowAllSubscriptionFilter, TopicSubscriptionFilter}, time_cache::DuplicateCache, @@ -183,7 +183,7 @@ enum PublishConfig { /// A strictly linearly increasing sequence number. /// -/// We start from the current time as unix timestamp in milliseconds. +/// We start from the current time as unix timestamp in nanoseconds. #[derive(Debug)] struct SequenceNumber(u64); @@ -194,7 +194,10 @@ impl SequenceNumber { .expect("time to be linear") .as_nanos(); - Self(unix_timestamp as u64) + Self( + u64::try_from(unix_timestamp) + .expect("timestamp in nanos since UNIX_EPOCH should fit in u64"), + ) } fn next(&mut self) -> u64 { @@ -322,10 +325,6 @@ pub struct Behaviour { /// Counts the number of `IWANT` that we sent the each peer since the last heartbeat. count_sent_iwant: HashMap, - /// Short term cache for published message ids. This is used for penalizing peers sending - /// our own messages back if the messages are anonymous or use a random author. - published_message_ids: DuplicateCache, - /// The filter used to handle message subscriptions. subscription_filter: F, @@ -360,18 +359,6 @@ where D::default(), ) } - - /// Allow the [`Behaviour`] to also record metrics. - /// Metrics can be evaluated by passing a reference to a [`Registry`]. - #[cfg(feature = "metrics")] - pub fn with_metrics( - mut self, - metrics_registry: &mut Registry, - metrics_config: MetricsConfig, - ) -> Self { - self.metrics = Some(Metrics::new(metrics_registry, metrics_config)); - self - } } impl Behaviour @@ -461,7 +448,6 @@ where count_received_ihave: HashMap::new(), count_sent_iwant: HashMap::new(), connected_peers: HashMap::new(), - published_message_ids: DuplicateCache::new(config.published_message_ids_cache_time()), config, subscription_filter, data_transform, @@ -469,6 +455,18 @@ where gossip_promises: Default::default(), }) } + + /// Allow the [`Behaviour`] to also record metrics. + /// Metrics can be evaluated by passing a reference to a [`Registry`]. + #[cfg(feature = "metrics")] + pub fn with_metrics( + mut self, + metrics_registry: &mut Registry, + metrics_config: MetricsConfig, + ) -> Self { + self.metrics = Some(Metrics::new(metrics_registry, metrics_config)); + self + } } impl Behaviour @@ -516,10 +514,9 @@ where /// Subscribe to a topic. /// - /// Returns [`Ok(true)`] if the subscription worked. Returns [`Ok(false)`] if we were already - /// subscribed. + /// Returns [`Ok(true)`](Ok) if the subscription worked. Returns [`Ok(false)`](Ok) if we were + /// already subscribed. pub fn subscribe(&mut self, topic: &Topic) -> Result { - tracing::debug!(%topic, "Subscribing to topic"); let topic_hash = topic.hash(); if !self.subscription_filter.can_subscribe(&topic_hash) { return Err(SubscriptionError::NotAllowed); @@ -548,7 +545,6 @@ where /// /// Returns `true` if we were subscribed to this topic. pub fn unsubscribe(&mut self, topic: &Topic) -> bool { - tracing::debug!(%topic, "Unsubscribing from topic"); let topic_hash = topic.hash(); if !self.mesh.contains_key(&topic_hash) { @@ -738,14 +734,6 @@ where // Consider the message as delivered for gossip promises. self.gossip_promises.message_delivered(&msg_id); - // If the message is anonymous or has a random author add it to the published message ids - // cache. - if let PublishConfig::RandomAuthor | PublishConfig::Anonymous = self.publish_config { - if !self.config.allow_self_origin() { - self.published_message_ids.insert(msg_id.clone()); - } - } - // Send to peers we know are subscribed to the topic. let mut publish_failed = true; for peer_id in recipient_peers.iter() { @@ -766,6 +754,7 @@ where if self.send_message( *peer_id, RpcOut::Publish { + message_id: msg_id.clone(), message: raw_message.clone(), timeout: Delay::new(self.config.publish_queue_duration()), }, @@ -978,8 +967,6 @@ where /// Gossipsub JOIN(topic) - adds topic peers to mesh and sends them GRAFT messages. fn join(&mut self, topic_hash: &TopicHash) { - tracing::debug!(topic=%topic_hash, "Running JOIN for topic"); - let mut added_peers = HashSet::new(); let mesh_n = self.config.mesh_n_for_topic(topic_hash); #[cfg(feature = "metrics")] @@ -987,6 +974,9 @@ where m.joined(topic_hash) } + // Always construct a mesh regardless if we find peers or not. + self.mesh.entry(topic_hash.clone()).or_default(); + // check if we have mesh_n peers in fanout[topic] and add them to the mesh if we do, // removing the fanout entry. if let Some((_, mut peers)) = self.fanout.remove_entry(topic_hash) { @@ -1355,6 +1345,7 @@ where self.send_message( *peer_id, RpcOut::Forward { + message_id: id.clone(), message: msg, timeout: Delay::new(self.config.forward_queue_duration()), }, @@ -1378,8 +1369,6 @@ where tracing::error!(peer_id = %peer_id, "Peer non-existent when handling graft"); return; }; - // Needs to be here to comply with the borrow checker. - let is_outbound = connected_peer.outbound; // For each topic, if a peer has grafted us, then we necessarily must be in their mesh // and they must be subscribed to the topic. Ensure we have recorded the mapping. @@ -1431,8 +1420,6 @@ where peer_score.add_penalty(peer_id, 1); // check the flood cutoff - // See: https://github.com/rust-lang/rust-clippy/issues/10061 - #[allow(unknown_lints, clippy::unchecked_duration_subtraction)] let flood_cutoff = (backoff_time + self.config.graft_flood_threshold()) - self.config.prune_backoff(); @@ -1467,10 +1454,9 @@ where } // check mesh upper bound and only allow graft if the upper bound is not reached - // or if it is an outbound peer let mesh_n_high = self.config.mesh_n_high_for_topic(&topic_hash); - if peers.len() >= mesh_n_high && !is_outbound { + if peers.len() >= mesh_n_high { to_prune_topics.insert(topic_hash.clone()); continue; } @@ -1720,7 +1706,7 @@ where own_id != propagation_source && raw_message.source.as_ref().is_some_and(|s| s == own_id) } else { - self.published_message_ids.contains(msg_id) + false }; if self_published { @@ -1907,7 +1893,7 @@ where subscriptions: &[Subscription], propagation_source: &PeerId, ) { - tracing::debug!( + tracing::trace!( source=%propagation_source, "Handling subscriptions: {:?}", subscriptions, @@ -2087,7 +2073,6 @@ where /// Heartbeat function which shifts the memcache and updates the mesh. fn heartbeat(&mut self) { - tracing::debug!("Starting heartbeat"); #[cfg(feature = "metrics")] let start = Instant::now(); @@ -2096,9 +2081,9 @@ where // steady-state size of the queues. #[cfg(feature = "metrics")] if let Some(m) = &mut self.metrics { - for sender_queue in self.connected_peers.values().map(|v| &v.sender) { - m.observe_priority_queue_size(sender_queue.priority_queue_len()); - m.observe_non_priority_queue_size(sender_queue.non_priority_queue_len()); + for sender_queue in self.connected_peers.values().map(|v| &v.messages) { + m.observe_priority_queue_size(sender_queue.priority_len()); + m.observe_non_priority_queue_size(sender_queue.non_priority_len()); } } @@ -2153,11 +2138,14 @@ where let mesh_n_high = self.config.mesh_n_high_for_topic(topic_hash); let mesh_outbound_min = self.config.mesh_outbound_min_for_topic(topic_hash); - // drop all peers with negative score, without PX - // if there is at some point a stable retain method for BTreeSet the following can be - // written more efficiently with retain. - let mut to_remove_peers = Vec::new(); - for peer_id in peers.iter() { + #[cfg(feature = "metrics")] + let mut removed_peers_count = 0; + + // Drop all peers with negative score, without PX + // + // TODO: Use `extract_if` once MSRV is raised to a version that includes its + // stabilization. + peers.retain(|peer_id| { let peer_score = scores.get(peer_id).map(|r| r.score).unwrap_or_default(); // Record the score per mesh @@ -2177,17 +2165,20 @@ where let current_topic = to_prune.entry(*peer_id).or_insert_with(Vec::new); current_topic.push(topic_hash.clone()); no_px.insert(*peer_id); - to_remove_peers.push(*peer_id); + + #[cfg(feature = "metrics")] + { + removed_peers_count += 1; + } + + return false; } - } + true + }); #[cfg(feature = "metrics")] if let Some(m) = self.metrics.as_mut() { - m.peers_removed(topic_hash, Churn::BadScore, to_remove_peers.len()) - } - - for peer_id in to_remove_peers { - peers.remove(&peer_id); + m.peers_removed(topic_hash, Churn::BadScore, removed_peers_count) } // too little peers - add some @@ -2221,7 +2212,7 @@ where } // too many peers - remove some - if peers.len() > mesh_n_high { + if peers.len() >= mesh_n_high { tracing::debug!( topic=%topic_hash, "HEARTBEAT: Mesh high. Topic contains: {} will reduce to: {}", @@ -2241,7 +2232,9 @@ where score_p1.partial_cmp(&score_p2).unwrap_or(Ordering::Equal) }); // shuffle everything except the last retain_scores many peers (the best ones) - shuffled[..peers.len() - self.config.retain_scores()].shuffle(&mut rng); + if peers.len() > self.config.retain_scores() { + shuffled[..peers.len() - self.config.retain_scores()].shuffle(&mut rng); + } // count total number of outbound peers let mut outbound = shuffled @@ -2473,8 +2466,10 @@ where get_random_peers(&self.connected_peers, topic_hash, needed_peers, |peer_id| { !peers.contains(peer_id) && !explicit_peers.contains(peer_id) - && scores.get(peer_id).map(|r| r.score).unwrap_or_default() - < publish_threshold + && !self + .peer_score + .below_threshold(peer_id, |ts| ts.publish_threshold) + .0 }); peers.extend(new_peers); } @@ -2512,6 +2507,11 @@ where // Report expired messages for (peer_id, failed_messages) in self.failed_messages.drain() { tracing::debug!("Peer couldn't consume messages: {:?}", failed_messages); + #[cfg(feature = "metrics")] + if let Some(metrics) = self.metrics.as_mut() { + metrics.observe_failed_priority_messages(failed_messages.priority); + metrics.observe_failed_non_priority_messages(failed_messages.non_priority); + } self.events .push_back(ToSwarm::GenerateEvent(Event::SlowPeer { peer_id, @@ -2531,7 +2531,6 @@ where } } - tracing::debug!("Completed Heartbeat"); #[cfg(feature = "metrics")] if let Some(metrics) = self.metrics.as_mut() { let duration = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); @@ -2760,6 +2759,7 @@ where self.send_message( *peer_id, RpcOut::Forward { + message_id: msg_id.clone(), message: message.clone(), timeout: Delay::new(self.config.forward_queue_duration()), }, @@ -2888,8 +2888,9 @@ where return false; } - // Try sending the message to the connection handler. - match peer.sender.send_message(rpc) { + // Try sending the message to the connection handler, + // High priority messages should not fail. + match peer.messages.try_push(rpc) { Ok(()) => true, Err(rpc) => { // Sending failed because the channel is full. @@ -2897,24 +2898,10 @@ where // Update failed message counter. let failed_messages = self.failed_messages.entry(peer_id).or_default(); - match rpc { - RpcOut::Publish { .. } => { - failed_messages.priority += 1; - failed_messages.publish += 1; - } - RpcOut::Forward { .. } => { - failed_messages.non_priority += 1; - failed_messages.forward += 1; - } - RpcOut::IWant(_) | RpcOut::IHave(_) | RpcOut::IDontWant(_) => { - failed_messages.non_priority += 1; - } - RpcOut::Graft(_) - | RpcOut::Prune(_) - | RpcOut::Subscribe(_) - | RpcOut::Unsubscribe(_) => { - unreachable!("Channel for highpriority control messages is unbounded and should always be open.") - } + if rpc.priority() { + failed_messages.priority += 1; + } else { + failed_messages.non_priority += 1; } // Update peer score. @@ -3143,16 +3130,19 @@ where kind: PeerKind::Floodsub, connections: vec![], outbound: false, - sender: Sender::new(self.config.connection_handler_queue_len()), + messages: Queue::new(self.config.connection_handler_queue_len()), topics: Default::default(), dont_send: LinkedHashMap::new(), }); // Add the new connection connected_peer.connections.push(connection_id); + // This clones a reference to the Queue so any new handlers reference the same underlying + // queue. No data is actually cloned here. Ok(Handler::new( + peer_id, self.config.protocol_config(), - connected_peer.sender.new_receiver(), + connected_peer.messages.clone(), )) } @@ -3170,16 +3160,19 @@ where // Diverging from the go implementation we only want to consider a peer as outbound peer // if its first connection is outbound. outbound: !self.px_peers.contains(&peer_id), - sender: Sender::new(self.config.connection_handler_queue_len()), + messages: Queue::new(self.config.connection_handler_queue_len()), topics: Default::default(), dont_send: LinkedHashMap::new(), }); // Add the new connection connected_peer.connections.push(connection_id); + // This clones a reference to the Queue so any new handlers reference the same underlying + // queue. No data is actually cloned here. Ok(Handler::new( + peer_id, self.config.protocol_config(), - connected_peer.sender.new_receiver(), + connected_peer.messages.clone(), )) } @@ -3221,6 +3214,8 @@ where } } } + // rpc is only used for metrics code. + #[allow(unused_variables)] HandlerEvent::MessageDropped(rpc) => { // Account for this in the scoring logic if let PeerScoreState::Active(peer_score) = &mut self.peer_score { @@ -3229,37 +3224,13 @@ where // Keep track of expired messages for the application layer. let failed_messages = self.failed_messages.entry(propagation_source).or_default(); - failed_messages.timeout += 1; - match rpc { - RpcOut::Publish { .. } => { - failed_messages.publish += 1; - } - RpcOut::Forward { .. } => { - failed_messages.forward += 1; - } - _ => {} - } - - // Record metrics on the failure. - #[cfg(feature = "metrics")] - if let Some(metrics) = self.metrics.as_mut() { - match rpc { - RpcOut::Publish { message, .. } => { - metrics.publish_msg_dropped(&message.topic); - metrics.timeout_msg_dropped(&message.topic); - } - RpcOut::Forward { message, .. } => { - metrics.forward_msg_dropped(&message.topic); - metrics.timeout_msg_dropped(&message.topic); - } - _ => {} - } - } + failed_messages.non_priority += 1; } HandlerEvent::Message { rpc, invalid_messages, } => { + tracing::debug!(peer=%propagation_source, message=?rpc, "Received gossipsub message"); // Handle the gossipsub RPC // Handle subscriptions @@ -3353,10 +3324,17 @@ where "Could not handle IDONTWANT, peer doesn't exist in connected peer list"); continue; }; + + // Remove messages from the queue. + #[allow(unused)] + let removed = peer.messages.remove_data_messages(&message_ids); + #[cfg(feature = "metrics")] if let Some(metrics) = self.metrics.as_mut() { metrics.register_idontwant(message_ids.len()); + metrics.register_removed_messages(removed); } + for message_id in message_ids { peer.dont_send.insert(message_id, Instant::now()); // Don't exceed capacity. diff --git a/protocols/gossipsub/src/behaviour/tests.rs b/protocols/gossipsub/src/behaviour/tests.rs deleted file mode 100644 index 6600f532a11..00000000000 --- a/protocols/gossipsub/src/behaviour/tests.rs +++ /dev/null @@ -1,6789 +0,0 @@ -// Copyright 2020 Sigma Prime Pty Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -// Collection of tests for the gossipsub network behaviour - -use std::{future, net::Ipv4Addr, thread::sleep}; - -use asynchronous_codec::{Decoder, Encoder}; -use byteorder::{BigEndian, ByteOrder}; -use bytes::BytesMut; -use libp2p_core::ConnectedPoint; -use rand::Rng; - -use super::*; -use crate::{ - config::{ConfigBuilder, TopicMeshConfig}, - protocol::GossipsubCodec, - rpc::Receiver, - subscription_filter::WhitelistSubscriptionFilter, - types::Rpc, - IdentTopic as Topic, -}; - -#[derive(Default, Debug)] -struct InjectNodes { - peer_no: usize, - topics: Vec, - to_subscribe: bool, - gs_config: Config, - explicit: usize, - outbound: usize, - scoring: Option<(PeerScoreParams, PeerScoreThresholds)>, - data_transform: D, - subscription_filter: F, - peer_kind: Option, -} - -impl InjectNodes -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - #[allow(clippy::type_complexity)] - pub(crate) fn create_network( - self, - ) -> ( - Behaviour, - Vec, - HashMap, - Vec, - ) { - let keypair = libp2p_identity::Keypair::generate_ed25519(); - // create a gossipsub struct - let mut gs: Behaviour = Behaviour::new_with_subscription_filter_and_transform( - MessageAuthenticity::Signed(keypair), - self.gs_config, - self.subscription_filter, - self.data_transform, - ) - .unwrap(); - - if let Some((scoring_params, scoring_thresholds)) = self.scoring { - gs.with_peer_score(scoring_params, scoring_thresholds) - .unwrap(); - } - - let mut topic_hashes = vec![]; - - // subscribe to the topics - for t in self.topics { - let topic = Topic::new(t); - gs.subscribe(&topic).unwrap(); - topic_hashes.push(topic.hash().clone()); - } - - // build and connect peer_no random peers - let mut peers = vec![]; - let mut receivers = HashMap::new(); - - let empty = vec![]; - for i in 0..self.peer_no { - let (peer, receiver) = add_peer_with_addr_and_kind( - &mut gs, - if self.to_subscribe { - &topic_hashes - } else { - &empty - }, - i < self.outbound, - i < self.explicit, - Multiaddr::empty(), - self.peer_kind.or(Some(PeerKind::Gossipsubv1_1)), - ); - peers.push(peer); - receivers.insert(peer, receiver); - } - - (gs, peers, receivers, topic_hashes) - } - - fn peer_no(mut self, peer_no: usize) -> Self { - self.peer_no = peer_no; - self - } - - fn topics(mut self, topics: Vec) -> Self { - self.topics = topics; - self - } - - #[allow(clippy::wrong_self_convention)] - fn to_subscribe(mut self, to_subscribe: bool) -> Self { - self.to_subscribe = to_subscribe; - self - } - - fn gs_config(mut self, gs_config: Config) -> Self { - self.gs_config = gs_config; - self - } - - fn explicit(mut self, explicit: usize) -> Self { - self.explicit = explicit; - self - } - - fn outbound(mut self, outbound: usize) -> Self { - self.outbound = outbound; - self - } - - fn scoring(mut self, scoring: Option<(PeerScoreParams, PeerScoreThresholds)>) -> Self { - self.scoring = scoring; - self - } - - fn subscription_filter(mut self, subscription_filter: F) -> Self { - self.subscription_filter = subscription_filter; - self - } - - fn peer_kind(mut self, peer_kind: PeerKind) -> Self { - self.peer_kind = Some(peer_kind); - self - } -} - -fn inject_nodes() -> InjectNodes -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - InjectNodes::default() -} - -fn inject_nodes1() -> InjectNodes { - InjectNodes::::default() -} - -// helper functions for testing - -fn add_peer( - gs: &mut Behaviour, - topic_hashes: &[TopicHash], - outbound: bool, - explicit: bool, -) -> (PeerId, Receiver) -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - add_peer_with_addr(gs, topic_hashes, outbound, explicit, Multiaddr::empty()) -} - -fn add_peer_with_addr( - gs: &mut Behaviour, - topic_hashes: &[TopicHash], - outbound: bool, - explicit: bool, - address: Multiaddr, -) -> (PeerId, Receiver) -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - add_peer_with_addr_and_kind( - gs, - topic_hashes, - outbound, - explicit, - address, - Some(PeerKind::Gossipsubv1_1), - ) -} - -fn add_peer_with_addr_and_kind( - gs: &mut Behaviour, - topic_hashes: &[TopicHash], - outbound: bool, - explicit: bool, - address: Multiaddr, - kind: Option, -) -> (PeerId, Receiver) -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - let peer = PeerId::random(); - let endpoint = if outbound { - ConnectedPoint::Dialer { - address, - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - } - } else { - ConnectedPoint::Listener { - local_addr: Multiaddr::empty(), - send_back_addr: address, - } - }; - - let sender = Sender::new(gs.config.connection_handler_queue_len()); - let receiver = sender.new_receiver(); - let connection_id = ConnectionId::new_unchecked(0); - gs.connected_peers.insert( - peer, - PeerDetails { - kind: kind.unwrap_or(PeerKind::Floodsub), - outbound, - connections: vec![connection_id], - topics: Default::default(), - sender, - dont_send: LinkedHashMap::new(), - }, - ); - - gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { - peer_id: peer, - connection_id, - endpoint: &endpoint, - failed_addresses: &[], - other_established: 0, // first connection - })); - if let Some(kind) = kind { - gs.on_connection_handler_event( - peer, - ConnectionId::new_unchecked(0), - HandlerEvent::PeerKind(kind), - ); - } - if explicit { - gs.add_explicit_peer(&peer); - } - if !topic_hashes.is_empty() { - gs.handle_received_subscriptions( - &topic_hashes - .iter() - .cloned() - .map(|t| Subscription { - action: SubscriptionAction::Subscribe, - topic_hash: t, - }) - .collect::>(), - &peer, - ); - } - (peer, receiver) -} - -fn disconnect_peer(gs: &mut Behaviour, peer_id: &PeerId) -where - D: DataTransform + Default + Clone + Send + 'static, - F: TopicSubscriptionFilter + Clone + Default + Send + 'static, -{ - if let Some(peer_connections) = gs.connected_peers.get(peer_id) { - let fake_endpoint = ConnectedPoint::Dialer { - address: Multiaddr::empty(), - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - }; // this is not relevant - // peer_connections.connections should never be empty. - - let mut active_connections = peer_connections.connections.len(); - for connection_id in peer_connections.connections.clone() { - active_connections = active_connections.checked_sub(1).unwrap(); - - gs.on_swarm_event(FromSwarm::ConnectionClosed(ConnectionClosed { - peer_id: *peer_id, - connection_id, - endpoint: &fake_endpoint, - remaining_established: active_connections, - cause: None, - })); - } - } -} - -// Converts a protobuf message into a gossipsub message for reading the Gossipsub event queue. -fn proto_to_message(rpc: &proto::RPC) -> Rpc { - // Store valid messages. - let mut messages = Vec::with_capacity(rpc.publish.len()); - let rpc = rpc.clone(); - for message in rpc.publish.into_iter() { - messages.push(RawMessage { - source: message.from.map(|x| PeerId::from_bytes(&x).unwrap()), - data: message.data.unwrap_or_default(), - sequence_number: message.seqno.map(|x| BigEndian::read_u64(&x)), /* don't inform the - * application */ - topic: TopicHash::from_raw(message.topic), - signature: message.signature, // don't inform the application - key: None, - validated: false, - }); - } - let mut control_msgs = Vec::new(); - if let Some(rpc_control) = rpc.control { - // Collect the gossipsub control messages - let ihave_msgs: Vec = rpc_control - .ihave - .into_iter() - .map(|ihave| { - ControlAction::IHave(IHave { - topic_hash: TopicHash::from_raw(ihave.topic_id.unwrap_or_default()), - message_ids: ihave - .message_ids - .into_iter() - .map(MessageId::from) - .collect::>(), - }) - }) - .collect(); - - let iwant_msgs: Vec = rpc_control - .iwant - .into_iter() - .map(|iwant| { - ControlAction::IWant(IWant { - message_ids: iwant - .message_ids - .into_iter() - .map(MessageId::from) - .collect::>(), - }) - }) - .collect(); - - let graft_msgs: Vec = rpc_control - .graft - .into_iter() - .map(|graft| { - ControlAction::Graft(Graft { - topic_hash: TopicHash::from_raw(graft.topic_id.unwrap_or_default()), - }) - }) - .collect(); - - let mut prune_msgs = Vec::new(); - - for prune in rpc_control.prune { - // filter out invalid peers - let peers = prune - .peers - .into_iter() - .filter_map(|info| { - info.peer_id - .and_then(|id| PeerId::from_bytes(&id).ok()) - .map(|peer_id| - //TODO signedPeerRecord, see https://github.com/libp2p/specs/pull/217 - PeerInfo { - peer_id: Some(peer_id), - }) - }) - .collect::>(); - - let topic_hash = TopicHash::from_raw(prune.topic_id.unwrap_or_default()); - prune_msgs.push(ControlAction::Prune(Prune { - topic_hash, - peers, - backoff: prune.backoff, - })); - } - - control_msgs.extend(ihave_msgs); - control_msgs.extend(iwant_msgs); - control_msgs.extend(graft_msgs); - control_msgs.extend(prune_msgs); - } - - Rpc { - messages, - subscriptions: rpc - .subscriptions - .into_iter() - .map(|sub| Subscription { - action: if Some(true) == sub.subscribe { - SubscriptionAction::Subscribe - } else { - SubscriptionAction::Unsubscribe - }, - topic_hash: TopicHash::from_raw(sub.topic_id.unwrap_or_default()), - }) - .collect(), - control_msgs, - } -} - -impl Behaviour { - fn as_peer_score_mut(&mut self) -> &mut PeerScore { - match self.peer_score { - PeerScoreState::Active(ref mut peer_score) => peer_score, - PeerScoreState::Disabled => panic!("PeerScore is deactivated"), - } - } -} -#[test] -/// Test local node subscribing to a topic -fn test_subscribe() { - // The node should: - // - Create an empty vector in mesh[topic] - // - Send subscription request to all peers - // - run JOIN(topic) - - let subscribe_topic = vec![String::from("test_subscribe")]; - let (gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(subscribe_topic) - .to_subscribe(true) - .create_network(); - - assert!( - gs.mesh.contains_key(&topic_hashes[0]), - "Subscribe should add a new entry to the mesh[topic] hashmap" - ); - - // collect all the subscriptions - let subscriptions = receivers - .into_values() - .fold(0, |mut collected_subscriptions, c| { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if let Ok(RpcOut::Subscribe(_)) = priority.try_recv() { - collected_subscriptions += 1 - } - } - collected_subscriptions - }); - - // we sent a subscribe to all known peers - assert_eq!(subscriptions, 20); -} - -/// Test unsubscribe. -#[test] -fn test_unsubscribe() { - // Unsubscribe should: - // - Remove the mesh entry for topic - // - Send UNSUBSCRIBE to all known peers - // - Call Leave - - let topic_strings = vec![String::from("topic1"), String::from("topic2")]; - let topics = topic_strings - .iter() - .map(|t| Topic::new(t.clone())) - .collect::>(); - - // subscribe to topic_strings - let (mut gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(topic_strings) - .to_subscribe(true) - .create_network(); - - for topic_hash in &topic_hashes { - assert!( - gs.connected_peers - .values() - .any(|p| p.topics.contains(topic_hash)), - "Topic_peers contain a topic entry" - ); - assert!( - gs.mesh.contains_key(topic_hash), - "mesh should contain a topic entry" - ); - } - - // unsubscribe from both topics - assert!( - gs.unsubscribe(&topics[0]), - "should be able to unsubscribe successfully from each topic", - ); - assert!( - gs.unsubscribe(&topics[1]), - "should be able to unsubscribe successfully from each topic", - ); - - // collect all the subscriptions - let subscriptions = receivers - .into_values() - .fold(0, |mut collected_subscriptions, c| { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if let Ok(RpcOut::Subscribe(_)) = priority.try_recv() { - collected_subscriptions += 1 - } - } - collected_subscriptions - }); - - // we sent a unsubscribe to all known peers, for two topics - assert_eq!(subscriptions, 40); - - // check we clean up internal structures - for topic_hash in &topic_hashes { - assert!( - !gs.mesh.contains_key(topic_hash), - "All topics should have been removed from the mesh" - ); - } -} - -/// Test JOIN(topic) functionality. -#[test] -fn test_join() { - // The Join function should: - // - Remove peers from fanout[topic] - // - Add any fanout[topic] peers to the mesh (up to mesh_n) - // - Fill up to mesh_n peers from known gossipsub peers in the topic - // - Send GRAFT messages to all nodes added to the mesh - - // This test is not an isolated unit test, rather it uses higher level, - // subscribe/unsubscribe to perform the test. - - let topic_strings = vec![String::from("topic1"), String::from("topic2")]; - let topics = topic_strings - .iter() - .map(|t| Topic::new(t.clone())) - .collect::>(); - - let (mut gs, _, mut receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(topic_strings) - .to_subscribe(true) - .create_network(); - - // Flush previous GRAFT messages. - receivers = flush_events(&mut gs, receivers); - - // unsubscribe, then call join to invoke functionality - assert!( - gs.unsubscribe(&topics[0]), - "should be able to unsubscribe successfully" - ); - assert!( - gs.unsubscribe(&topics[1]), - "should be able to unsubscribe successfully" - ); - - // re-subscribe - there should be peers associated with the topic - assert!( - gs.subscribe(&topics[0]).unwrap(), - "should be able to subscribe successfully" - ); - - // should have added mesh_n nodes to the mesh - assert!( - gs.mesh.get(&topic_hashes[0]).unwrap().len() == 6, - "Should have added 6 nodes to the mesh" - ); - - fn count_grafts(receivers: HashMap) -> (usize, HashMap) { - let mut new_receivers = HashMap::new(); - let mut acc = 0; - - for (peer_id, c) in receivers.into_iter() { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if let Ok(RpcOut::Graft(_)) = priority.try_recv() { - acc += 1; - } - } - new_receivers.insert( - peer_id, - Receiver { - priority_queue_len: c.priority_queue_len, - priority: c.priority, - non_priority: c.non_priority, - }, - ); - } - (acc, new_receivers) - } - - // there should be mesh_n GRAFT messages. - let (graft_messages, mut receivers) = count_grafts(receivers); - - assert_eq!( - graft_messages, 6, - "There should be 6 grafts messages sent to peers" - ); - - // verify fanout nodes - // add 3 random peers to the fanout[topic1] - gs.fanout - .insert(topic_hashes[1].clone(), Default::default()); - let mut new_peers: Vec = vec![]; - - for _ in 0..3 { - let random_peer = PeerId::random(); - // inform the behaviour of a new peer - let address = "/ip4/127.0.0.1".parse::().unwrap(); - gs.handle_established_inbound_connection( - ConnectionId::new_unchecked(0), - random_peer, - &address, - &address, - ) - .unwrap(); - let sender = Sender::new(gs.config.connection_handler_queue_len()); - let receiver = sender.new_receiver(); - let connection_id = ConnectionId::new_unchecked(0); - gs.connected_peers.insert( - random_peer, - PeerDetails { - kind: PeerKind::Floodsub, - outbound: false, - connections: vec![connection_id], - topics: Default::default(), - sender, - dont_send: LinkedHashMap::new(), - }, - ); - receivers.insert(random_peer, receiver); - - gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { - peer_id: random_peer, - connection_id, - endpoint: &ConnectedPoint::Dialer { - address, - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - }, - failed_addresses: &[], - other_established: 0, - })); - - // add the new peer to the fanout - let fanout_peers = gs.fanout.get_mut(&topic_hashes[1]).unwrap(); - fanout_peers.insert(random_peer); - new_peers.push(random_peer); - } - - // subscribe to topic1 - gs.subscribe(&topics[1]).unwrap(); - - // the three new peers should have been added, along with 3 more from the pool. - assert!( - gs.mesh.get(&topic_hashes[1]).unwrap().len() == 6, - "Should have added 6 nodes to the mesh" - ); - let mesh_peers = gs.mesh.get(&topic_hashes[1]).unwrap(); - for new_peer in new_peers { - assert!( - mesh_peers.contains(&new_peer), - "Fanout peer should be included in the mesh" - ); - } - - // there should now 6 graft messages to be sent - let (graft_messages, _) = count_grafts(receivers); - - assert_eq!( - graft_messages, 6, - "There should be 6 grafts messages sent to peers" - ); -} - -/// Test local node publish to subscribed topic -#[test] -fn test_publish_without_flood_publishing() { - // node should: - // - Send publish message to all peers - // - Insert message into gs.mcache and gs.received - - // turn off flood publish to test old behaviour - let config = ConfigBuilder::default() - .flood_publish(false) - .build() - .unwrap(); - - let publish_topic = String::from("test_publish"); - let (mut gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![publish_topic.clone()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - assert!( - gs.mesh.contains_key(&topic_hashes[0]), - "Subscribe should add a new entry to the mesh[topic] hashmap" - ); - - // all peers should be subscribed to the topic - assert_eq!( - gs.connected_peers - .values() - .filter(|p| p.topics.contains(&topic_hashes[0])) - .count(), - 20, - "Peers should be subscribed to the topic" - ); - - // publish on topic - let publish_data = vec![0; 42]; - gs.publish(Topic::new(publish_topic), publish_data).unwrap(); - - // Collect all publish messages - let publishes = receivers - .into_values() - .fold(vec![], |mut collected_publish, c| { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { - collected_publish.push(message); - } - } - collected_publish - }); - - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform( - publishes - .first() - .expect("Should contain > 0 entries") - .clone(), - ) - .unwrap(); - - let msg_id = gs.config.message_id(message); - - let config: Config = Config::default(); - assert_eq!( - publishes.len(), - config.mesh_n(), - "Should send a publish message to at least mesh_n peers" - ); - - assert!( - gs.mcache.get(&msg_id).is_some(), - "Message cache should contain published message" - ); -} - -/// Test local node publish to unsubscribed topic -#[test] -fn test_fanout() { - // node should: - // - Populate fanout peers - // - Send publish message to fanout peers - // - Insert message into gs.mcache and gs.received - - // turn off flood publish to test fanout behaviour - let config = ConfigBuilder::default() - .flood_publish(false) - .build() - .unwrap(); - - let fanout_topic = String::from("test_fanout"); - let (mut gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![fanout_topic.clone()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - assert!( - gs.mesh.contains_key(&topic_hashes[0]), - "Subscribe should add a new entry to the mesh[topic] hashmap" - ); - // Unsubscribe from topic - assert!( - gs.unsubscribe(&Topic::new(fanout_topic.clone())), - "should be able to unsubscribe successfully from topic" - ); - - // Publish on unsubscribed topic - let publish_data = vec![0; 42]; - gs.publish(Topic::new(fanout_topic.clone()), publish_data) - .unwrap(); - - assert_eq!( - gs.fanout - .get(&TopicHash::from_raw(fanout_topic)) - .unwrap() - .len(), - gs.config.mesh_n(), - "Fanout should contain `mesh_n` peers for fanout topic" - ); - - // Collect all publish messages - let publishes = receivers - .into_values() - .fold(vec![], |mut collected_publish, c| { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { - collected_publish.push(message); - } - } - collected_publish - }); - - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform( - publishes - .first() - .expect("Should contain > 0 entries") - .clone(), - ) - .unwrap(); - - let msg_id = gs.config.message_id(message); - - assert_eq!( - publishes.len(), - gs.config.mesh_n(), - "Should send a publish message to `mesh_n` fanout peers" - ); - - assert!( - gs.mcache.get(&msg_id).is_some(), - "Message cache should contain published message" - ); -} - -/// Test the gossipsub NetworkBehaviour peer connection logic. -#[test] -fn test_inject_connected() { - let (gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1"), String::from("topic2")]) - .to_subscribe(true) - .create_network(); - - // check that our subscriptions are sent to each of the peers - // collect all the SendEvents - let subscriptions = receivers.into_iter().fold( - HashMap::>::new(), - |mut collected_subscriptions, (peer, c)| { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if let Ok(RpcOut::Subscribe(topic)) = priority.try_recv() { - let mut peer_subs = collected_subscriptions.remove(&peer).unwrap_or_default(); - peer_subs.push(topic.into_string()); - collected_subscriptions.insert(peer, peer_subs); - } - } - collected_subscriptions - }, - ); - - // check that there are two subscriptions sent to each peer - for peer_subs in subscriptions.values() { - assert!(peer_subs.contains(&String::from("topic1"))); - assert!(peer_subs.contains(&String::from("topic2"))); - assert_eq!(peer_subs.len(), 2); - } - - // check that there are 20 send events created - assert_eq!(subscriptions.len(), 20); - - // should add the new peers to `peer_topics` with an empty vec as a gossipsub node - for peer in peers { - let peer = gs.connected_peers.get(&peer).unwrap(); - assert!( - peer.topics == topic_hashes.iter().cloned().collect(), - "The topics for each node should all topics" - ); - } -} - -/// Test subscription handling -#[test] -fn test_handle_received_subscriptions() { - // For every subscription: - // SUBSCRIBE: - Add subscribed topic to peer_topics for peer. - // - Add peer to topics_peer. - // UNSUBSCRIBE - Remove topic from peer_topics for peer. - // - Remove peer from topic_peers. - - let topics = ["topic1", "topic2", "topic3", "topic4"] - .iter() - .map(|&t| String::from(t)) - .collect(); - let (mut gs, peers, _receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(topics) - .to_subscribe(false) - .create_network(); - - // The first peer sends 3 subscriptions and 1 unsubscription - let mut subscriptions = topic_hashes[..3] - .iter() - .map(|topic_hash| Subscription { - action: SubscriptionAction::Subscribe, - topic_hash: topic_hash.clone(), - }) - .collect::>(); - - subscriptions.push(Subscription { - action: SubscriptionAction::Unsubscribe, - topic_hash: topic_hashes[topic_hashes.len() - 1].clone(), - }); - - let unknown_peer = PeerId::random(); - // process the subscriptions - // first and second peers send subscriptions - gs.handle_received_subscriptions(&subscriptions, &peers[0]); - gs.handle_received_subscriptions(&subscriptions, &peers[1]); - // unknown peer sends the same subscriptions - gs.handle_received_subscriptions(&subscriptions, &unknown_peer); - - // verify the result - - let peer = gs.connected_peers.get(&peers[0]).unwrap(); - assert!( - peer.topics - == topic_hashes - .iter() - .take(3) - .cloned() - .collect::>(), - "First peer should be subscribed to three topics" - ); - let peer1 = gs.connected_peers.get(&peers[1]).unwrap(); - assert!( - peer1.topics - == topic_hashes - .iter() - .take(3) - .cloned() - .collect::>(), - "Second peer should be subscribed to three topics" - ); - - assert!( - !gs.connected_peers.contains_key(&unknown_peer), - "Unknown peer should not have been added" - ); - - for topic_hash in topic_hashes[..3].iter() { - let topic_peers = gs - .connected_peers - .iter() - .filter(|(_, p)| p.topics.contains(topic_hash)) - .map(|(peer_id, _)| *peer_id) - .collect::>(); - assert!( - topic_peers == peers[..2].iter().cloned().collect(), - "Two peers should be added to the first three topics" - ); - } - - // Peer 0 unsubscribes from the first topic - - gs.handle_received_subscriptions( - &[Subscription { - action: SubscriptionAction::Unsubscribe, - topic_hash: topic_hashes[0].clone(), - }], - &peers[0], - ); - - let peer = gs.connected_peers.get(&peers[0]).unwrap(); - assert!( - peer.topics == topic_hashes[1..3].iter().cloned().collect::>(), - "Peer should be subscribed to two topics" - ); - - // only gossipsub at the moment - let topic_peers = gs - .connected_peers - .iter() - .filter(|(_, p)| p.topics.contains(&topic_hashes[0])) - .map(|(peer_id, _)| *peer_id) - .collect::>(); - - assert!( - topic_peers == peers[1..2].iter().cloned().collect(), - "Only the second peers should be in the first topic" - ); -} - -/// Test Gossipsub.get_random_peers() function -#[test] -fn test_get_random_peers() { - // generate a default Config - let gs_config = ConfigBuilder::default() - .validation_mode(ValidationMode::Anonymous) - .build() - .unwrap(); - // create a gossipsub struct - let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::Anonymous, gs_config).unwrap(); - - // create a topic and fill it with some peers - let topic_hash = Topic::new("Test").hash(); - let mut peers = vec![]; - let mut topics = BTreeSet::new(); - topics.insert(topic_hash.clone()); - - for _ in 0..20 { - let peer_id = PeerId::random(); - peers.push(peer_id); - gs.connected_peers.insert( - peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(gs.config.connection_handler_queue_len()), - dont_send: LinkedHashMap::new(), - }, - ); - } - - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 5, |_| true); - assert_eq!(random_peers.len(), 5, "Expected 5 peers to be returned"); - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 30, |_| true); - assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); - assert!( - random_peers == peers.iter().cloned().collect(), - "Expected no shuffling" - ); - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 20, |_| true); - assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); - assert!( - random_peers == peers.iter().cloned().collect(), - "Expected no shuffling" - ); - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 0, |_| true); - assert!(random_peers.is_empty(), "Expected 0 peers to be returned"); - // test the filter - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 5, |_| false); - assert!(random_peers.is_empty(), "Expected 0 peers to be returned"); - let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 10, { - |peer| peers.contains(peer) - }); - assert!(random_peers.len() == 10, "Expected 10 peers to be returned"); -} - -/// Tests that the correct message is sent when a peer asks for a message in our cache. -#[test] -fn test_handle_iwant_msg_cached() { - let (mut gs, peers, receivers, _) = inject_nodes1() - .peer_no(20) - .topics(Vec::new()) - .to_subscribe(true) - .create_network(); - - let raw_message = RawMessage { - source: Some(peers[11]), - data: vec![1, 2, 3, 4], - sequence_number: Some(1u64), - topic: TopicHash::from_raw("topic"), - signature: None, - key: None, - validated: true, - }; - - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform(raw_message.clone()) - .unwrap(); - - let msg_id = gs.config.message_id(message); - gs.mcache.put(&msg_id, raw_message); - - gs.handle_iwant(&peers[7], vec![msg_id.clone()]); - - // the messages we are sending - let sent_messages = receivers - .into_values() - .fold(vec![], |mut collected_messages, c| { - let non_priority = c.non_priority.get_ref(); - while !non_priority.is_empty() { - if let Ok(RpcOut::Forward { message, .. }) = non_priority.try_recv() { - collected_messages.push(message) - } - } - collected_messages - }); - - assert!( - sent_messages - .iter() - .map(|msg| gs.data_transform.inbound_transform(msg.clone()).unwrap()) - .any(|msg| gs.config.message_id(&msg) == msg_id), - "Expected the cached message to be sent to an IWANT peer" - ); -} - -/// Tests that messages are sent correctly depending on the shifting of the message cache. -#[test] -fn test_handle_iwant_msg_cached_shifted() { - let (mut gs, peers, mut receivers, _) = inject_nodes1() - .peer_no(20) - .topics(Vec::new()) - .to_subscribe(true) - .create_network(); - - // perform 10 memshifts and check that it leaves the cache - for shift in 1..10 { - let raw_message = RawMessage { - source: Some(peers[11]), - data: vec![1, 2, 3, 4], - sequence_number: Some(shift), - topic: TopicHash::from_raw("topic"), - signature: None, - key: None, - validated: true, - }; - - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform(raw_message.clone()) - .unwrap(); - - let msg_id = gs.config.message_id(message); - gs.mcache.put(&msg_id, raw_message); - for _ in 0..shift { - gs.mcache.shift(); - } - - gs.handle_iwant(&peers[7], vec![msg_id.clone()]); - - // is the message is being sent? - let mut message_exists = false; - receivers = receivers.into_iter().map(|(peer_id, c)| { - let non_priority = c.non_priority.get_ref(); - while !non_priority.is_empty() { - if matches!(non_priority.try_recv(), Ok(RpcOut::Forward{message, timeout: _ }) if - gs.config.message_id( - &gs.data_transform - .inbound_transform(message.clone()) - .unwrap(), - ) == msg_id) - { - message_exists = true; - } - } - ( - peer_id, - Receiver { - priority_queue_len: c.priority_queue_len, - priority: c.priority, - non_priority: c.non_priority, - }, - ) - }).collect(); - // default history_length is 5, expect no messages after shift > 5 - if shift < 5 { - assert!( - message_exists, - "Expected the cached message to be sent to an IWANT peer before 5 shifts" - ); - } else { - assert!( - !message_exists, - "Expected the cached message to not be sent to an IWANT peer after 5 shifts" - ); - } - } -} - -/// tests that an event is not created when a peers asks for a message not in our cache -#[test] -fn test_handle_iwant_msg_not_cached() { - let (mut gs, peers, _, _) = inject_nodes1() - .peer_no(20) - .topics(Vec::new()) - .to_subscribe(true) - .create_network(); - - let events_before = gs.events.len(); - gs.handle_iwant(&peers[7], vec![MessageId::new(b"unknown id")]); - let events_after = gs.events.len(); - - assert_eq!( - events_before, events_after, - "Expected event count to stay the same" - ); -} - -#[test] -fn test_handle_iwant_msg_but_already_sent_idontwant() { - let (mut gs, peers, receivers, _) = inject_nodes1() - .peer_no(20) - .topics(Vec::new()) - .to_subscribe(true) - .create_network(); - - let raw_message = RawMessage { - source: Some(peers[11]), - data: vec![1, 2, 3, 4], - sequence_number: Some(1u64), - topic: TopicHash::from_raw("topic"), - signature: None, - key: None, - validated: true, - }; - - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform(raw_message.clone()) - .unwrap(); - - let msg_id = gs.config.message_id(message); - gs.mcache.put(&msg_id, raw_message); - - // Receive IDONTWANT from Peer 1. - let rpc = Rpc { - messages: vec![], - subscriptions: vec![], - control_msgs: vec![ControlAction::IDontWant(IDontWant { - message_ids: vec![msg_id.clone()], - })], - }; - gs.on_connection_handler_event( - peers[1], - ConnectionId::new_unchecked(0), - HandlerEvent::Message { - rpc, - invalid_messages: vec![], - }, - ); - - // Receive IWANT from Peer 1. - gs.handle_iwant(&peers[1], vec![msg_id.clone()]); - - // Check that no messages are sent. - receivers.iter().for_each(|(_, receiver)| { - assert!(receiver.non_priority.get_ref().is_empty()); - }); -} - -/// tests that an event is created when a peer shares that it has a message we want -#[test] -fn test_handle_ihave_subscribed_and_msg_not_cached() { - let (mut gs, peers, mut receivers, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - - gs.handle_ihave( - &peers[7], - vec![(topic_hashes[0].clone(), vec![MessageId::new(b"unknown id")])], - ); - - // check that we sent an IWANT request for `unknown id` - let mut iwant_exists = false; - let receiver = receivers.remove(&peers[7]).unwrap(); - let non_priority = receiver.non_priority.get_ref(); - while !non_priority.is_empty() { - if let Ok(RpcOut::IWant(IWant { message_ids })) = non_priority.try_recv() { - if message_ids - .iter() - .any(|m| *m == MessageId::new(b"unknown id")) - { - iwant_exists = true; - break; - } - } - } - - assert!( - iwant_exists, - "Expected to send an IWANT control message for unknown message id" - ); -} - -/// tests that an event is not created when a peer shares that it has a message that -/// we already have -#[test] -fn test_handle_ihave_subscribed_and_msg_cached() { - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - - let msg_id = MessageId::new(b"known id"); - - let events_before = gs.events.len(); - gs.handle_ihave(&peers[7], vec![(topic_hashes[0].clone(), vec![msg_id])]); - let events_after = gs.events.len(); - - assert_eq!( - events_before, events_after, - "Expected event count to stay the same" - ) -} - -/// test that an event is not created when a peer shares that it has a message in -/// a topic that we are not subscribed to -#[test] -fn test_handle_ihave_not_subscribed() { - let (mut gs, peers, _, _) = inject_nodes1() - .peer_no(20) - .topics(vec![]) - .to_subscribe(true) - .create_network(); - - let events_before = gs.events.len(); - gs.handle_ihave( - &peers[7], - vec![( - TopicHash::from_raw(String::from("unsubscribed topic")), - vec![MessageId::new(b"irrelevant id")], - )], - ); - let events_after = gs.events.len(); - - assert_eq!( - events_before, events_after, - "Expected event count to stay the same" - ) -} - -/// tests that a peer is added to our mesh when we are both subscribed -/// to the same topic -#[test] -fn test_handle_graft_is_subscribed() { - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - - gs.handle_graft(&peers[7], topic_hashes.clone()); - - assert!( - gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), - "Expected peer to have been added to mesh" - ); -} - -/// tests that a peer is not added to our mesh when they are subscribed to -/// a topic that we are not -#[test] -fn test_handle_graft_is_not_subscribed() { - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - - gs.handle_graft( - &peers[7], - vec![TopicHash::from_raw(String::from("unsubscribed topic"))], - ); - - assert!( - !gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), - "Expected peer to have been added to mesh" - ); -} - -/// tests multiple topics in a single graft message -#[test] -fn test_handle_graft_multiple_topics() { - let topics: Vec = ["topic1", "topic2", "topic3", "topic4"] - .iter() - .map(|&t| String::from(t)) - .collect(); - - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(topics) - .to_subscribe(true) - .create_network(); - - let mut their_topics = topic_hashes.clone(); - // their_topics = [topic1, topic2, topic3] - // our_topics = [topic1, topic2, topic4] - their_topics.pop(); - gs.leave(&their_topics[2]); - - gs.handle_graft(&peers[7], their_topics.clone()); - - for hash in topic_hashes.iter().take(2) { - assert!( - gs.mesh.get(hash).unwrap().contains(&peers[7]), - "Expected peer to be in the mesh for the first 2 topics" - ); - } - - assert!( - !gs.mesh.contains_key(&topic_hashes[2]), - "Expected the second topic to not be in the mesh" - ); -} - -/// tests that a peer is removed from our mesh -#[test] -fn test_handle_prune_peer_in_mesh() { - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(20) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - - // insert peer into our mesh for 'topic1' - gs.mesh - .insert(topic_hashes[0].clone(), peers.iter().cloned().collect()); - assert!( - gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), - "Expected peer to be in mesh" - ); - - gs.handle_prune( - &peers[7], - topic_hashes - .iter() - .map(|h| (h.clone(), vec![], None)) - .collect(), - ); - assert!( - !gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), - "Expected peer to be removed from mesh" - ); -} - -fn count_control_msgs( - receivers: HashMap, - mut filter: impl FnMut(&PeerId, &RpcOut) -> bool, -) -> (usize, HashMap) { - let mut new_receivers = HashMap::new(); - let mut collected_messages = 0; - for (peer_id, c) in receivers.into_iter() { - let priority = c.priority.get_ref(); - let non_priority = c.non_priority.get_ref(); - while !priority.is_empty() || !non_priority.is_empty() { - if let Ok(rpc) = priority.try_recv() { - if filter(&peer_id, &rpc) { - collected_messages += 1; - } - } - if let Ok(rpc) = non_priority.try_recv() { - if filter(&peer_id, &rpc) { - collected_messages += 1; - } - } - } - new_receivers.insert( - peer_id, - Receiver { - priority_queue_len: c.priority_queue_len, - priority: c.priority, - non_priority: c.non_priority, - }, - ); - } - (collected_messages, new_receivers) -} - -fn flush_events( - gs: &mut Behaviour, - receivers: HashMap, -) -> HashMap { - gs.events.clear(); - let mut new_receivers = HashMap::new(); - for (peer_id, c) in receivers.into_iter() { - let priority = c.priority.get_ref(); - let non_priority = c.non_priority.get_ref(); - while !priority.is_empty() || !non_priority.is_empty() { - let _ = priority.try_recv(); - let _ = non_priority.try_recv(); - } - new_receivers.insert( - peer_id, - Receiver { - priority_queue_len: c.priority_queue_len, - priority: c.priority, - non_priority: c.non_priority, - }, - ); - } - new_receivers -} - -/// tests that a peer added as explicit peer gets connected to -#[test] -fn test_explicit_peer_gets_connected() { - let (mut gs, _, _, _) = inject_nodes1() - .peer_no(0) - .topics(Vec::new()) - .to_subscribe(true) - .create_network(); - - // create new peer - let peer = PeerId::random(); - - // add peer as explicit peer - gs.add_explicit_peer(&peer); - - let num_events = gs - .events - .iter() - .filter(|e| match e { - ToSwarm::Dial { opts } => opts.get_peer_id() == Some(peer), - _ => false, - }) - .count(); - - assert_eq!( - num_events, 1, - "There was no dial peer event for the explicit peer" - ); -} - -#[test] -fn test_explicit_peer_reconnects() { - let config = ConfigBuilder::default() - .check_explicit_peers_ticks(2) - .build() - .unwrap(); - let (mut gs, others, receivers, _) = inject_nodes1() - .peer_no(1) - .topics(Vec::new()) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - let peer = others.first().unwrap(); - - // add peer as explicit peer - gs.add_explicit_peer(peer); - - flush_events(&mut gs, receivers); - - // disconnect peer - disconnect_peer(&mut gs, peer); - - gs.heartbeat(); - - // check that no reconnect after first heartbeat since `explicit_peer_ticks == 2` - assert_eq!( - gs.events - .iter() - .filter(|e| match e { - ToSwarm::Dial { opts } => opts.get_peer_id() == Some(*peer), - _ => false, - }) - .count(), - 0, - "There was a dial peer event before explicit_peer_ticks heartbeats" - ); - - gs.heartbeat(); - - // check that there is a reconnect after second heartbeat - assert!( - gs.events - .iter() - .filter(|e| match e { - ToSwarm::Dial { opts } => opts.get_peer_id() == Some(*peer), - _ => false, - }) - .count() - >= 1, - "There was no dial peer event for the explicit peer" - ); -} - -#[test] -fn test_handle_graft_explicit_peer() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(1) - .topics(vec![String::from("topic1"), String::from("topic2")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - let peer = peers.first().unwrap(); - - gs.handle_graft(peer, topic_hashes.clone()); - - // peer got not added to mesh - assert!(gs.mesh[&topic_hashes[0]].is_empty()); - assert!(gs.mesh[&topic_hashes[1]].is_empty()); - - // check prunes - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == peer - && match m { - RpcOut::Prune(Prune { topic_hash, .. }) => { - topic_hash == &topic_hashes[0] || topic_hash == &topic_hashes[1] - } - _ => false, - } - }); - assert!( - control_msgs >= 2, - "Not enough prunes sent when grafting from explicit peer" - ); -} - -#[test] -fn explicit_peers_not_added_to_mesh_on_receiving_subscription() { - let (gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(2) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - // only peer 1 is in the mesh not peer 0 (which is an explicit peer) - assert_eq!( - gs.mesh[&topic_hashes[0]], - vec![peers[1]].into_iter().collect() - ); - - // assert that graft gets created to non-explicit peer - let (control_msgs, receivers) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) - }); - assert!( - control_msgs >= 1, - "No graft message got created to non-explicit peer" - ); - - // assert that no graft gets created to explicit peer - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) - }); - assert_eq!( - control_msgs, 0, - "A graft message got created to an explicit peer" - ); -} - -#[test] -fn do_not_graft_explicit_peer() { - let (mut gs, others, receivers, topic_hashes) = inject_nodes1() - .peer_no(1) - .topics(vec![String::from("topic")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - gs.heartbeat(); - - // mesh stays empty - assert_eq!(gs.mesh[&topic_hashes[0]], BTreeSet::new()); - - // assert that no graft gets created to explicit peer - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &others[0] && matches!(m, RpcOut::Graft { .. }) - }); - assert_eq!( - control_msgs, 0, - "A graft message got created to an explicit peer" - ); -} - -#[test] -fn do_forward_messages_to_explicit_peers() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(2) - .topics(vec![String::from("topic1"), String::from("topic2")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - let local_id = PeerId::random(); - - let message = RawMessage { - source: Some(peers[1]), - data: vec![12], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(message.clone(), &local_id); - assert_eq!( - receivers.into_iter().fold(0, |mut fwds, (peer_id, c)| { - let non_priority = c.non_priority.get_ref(); - while !non_priority.is_empty() { - if matches!(non_priority.try_recv(), Ok(RpcOut::Forward{message: m, timeout: _}) if peer_id == peers[0] && m.data == message.data) { - fwds +=1; - } - } - fwds - }), - 1, - "The message did not get forwarded to the explicit peer" - ); -} - -#[test] -fn explicit_peers_not_added_to_mesh_on_subscribe() { - let (mut gs, peers, receivers, _) = inject_nodes1() - .peer_no(2) - .topics(Vec::new()) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - // create new topic, both peers subscribing to it but we do not subscribe to it - let topic = Topic::new(String::from("t")); - let topic_hash = topic.hash(); - for peer in peers.iter().take(2) { - gs.handle_received_subscriptions( - &[Subscription { - action: SubscriptionAction::Subscribe, - topic_hash: topic_hash.clone(), - }], - peer, - ); - } - - // subscribe now to topic - gs.subscribe(&topic).unwrap(); - - // only peer 1 is in the mesh not peer 0 (which is an explicit peer) - assert_eq!(gs.mesh[&topic_hash], vec![peers[1]].into_iter().collect()); - - // assert that graft gets created to non-explicit peer - let (control_msgs, receivers) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) - }); - assert!( - control_msgs > 0, - "No graft message got created to non-explicit peer" - ); - - // assert that no graft gets created to explicit peer - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) - }); - assert_eq!( - control_msgs, 0, - "A graft message got created to an explicit peer" - ); -} - -#[test] -fn explicit_peers_not_added_to_mesh_from_fanout_on_subscribe() { - let (mut gs, peers, receivers, _) = inject_nodes1() - .peer_no(2) - .topics(Vec::new()) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - // create new topic, both peers subscribing to it but we do not subscribe to it - let topic = Topic::new(String::from("t")); - let topic_hash = topic.hash(); - for peer in peers.iter().take(2) { - gs.handle_received_subscriptions( - &[Subscription { - action: SubscriptionAction::Subscribe, - topic_hash: topic_hash.clone(), - }], - peer, - ); - } - - // we send a message for this topic => this will initialize the fanout - gs.publish(topic.clone(), vec![1, 2, 3]).unwrap(); - - // subscribe now to topic - gs.subscribe(&topic).unwrap(); - - // only peer 1 is in the mesh not peer 0 (which is an explicit peer) - assert_eq!(gs.mesh[&topic_hash], vec![peers[1]].into_iter().collect()); - - // assert that graft gets created to non-explicit peer - let (control_msgs, receivers) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) - }); - assert!( - control_msgs >= 1, - "No graft message got created to non-explicit peer" - ); - - // assert that no graft gets created to explicit peer - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) - }); - assert_eq!( - control_msgs, 0, - "A graft message got created to an explicit peer" - ); -} - -#[test] -fn no_gossip_gets_sent_to_explicit_peers() { - let (mut gs, peers, mut receivers, topic_hashes) = inject_nodes1() - .peer_no(2) - .topics(vec![String::from("topic1"), String::from("topic2")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .create_network(); - - let local_id = PeerId::random(); - - let message = RawMessage { - source: Some(peers[1]), - data: vec![], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - - // forward the message - gs.handle_received_message(message, &local_id); - - // simulate multiple gossip calls (for randomness) - for _ in 0..3 { - gs.emit_gossip(); - } - - // assert that no gossip gets sent to explicit peer - let receiver = receivers.remove(&peers[0]).unwrap(); - let mut gossips = 0; - let non_priority = receiver.non_priority.get_ref(); - while !non_priority.is_empty() { - if let Ok(RpcOut::IHave(_)) = non_priority.try_recv() { - gossips += 1; - } - } - assert_eq!(gossips, 0, "Gossip got emitted to explicit peer"); -} - -/// Tests the mesh maintenance addition -#[test] -fn test_mesh_addition() { - let config: Config = Config::default(); - - // Adds mesh_low peers and PRUNE 2 giving us a deficit. - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n() + 1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - let to_remove_peers = config.mesh_n() + 1 - config.mesh_n_low() - 1; - - for peer in peers.iter().take(to_remove_peers) { - gs.handle_prune( - peer, - topics.iter().map(|h| (h.clone(), vec![], None)).collect(), - ); - } - - // Verify the pruned peers are removed from the mesh. - assert_eq!( - gs.mesh.get(&topics[0]).unwrap().len(), - config.mesh_n_low() - 1 - ); - - // run a heartbeat - gs.heartbeat(); - - // Peers should be added to reach mesh_n - assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), config.mesh_n()); -} - -/// Tests the mesh maintenance subtraction -#[test] -fn test_mesh_subtraction() { - let config = Config::default(); - - // Adds mesh_low peers and PRUNE 2 giving us a deficit. - let n = config.mesh_n_high() + 10; - // make all outbound connections so that we allow grafting to all - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(n) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .outbound(n) - .create_network(); - - // graft all the peers - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // run a heartbeat - gs.heartbeat(); - - // Peers should be removed to reach mesh_n - assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), config.mesh_n()); -} - -#[test] -fn test_connect_to_px_peers_on_handle_prune() { - let config: Config = Config::default(); - - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - // handle prune from single peer with px peers - - let mut px = Vec::new(); - // propose more px peers than config.prune_peers() - for _ in 0..config.prune_peers() + 5 { - px.push(PeerInfo { - peer_id: Some(PeerId::random()), - }); - } - - gs.handle_prune( - &peers[0], - vec![( - topics[0].clone(), - px.clone(), - Some(config.prune_backoff().as_secs()), - )], - ); - - // Check DialPeer events for px peers - let dials: Vec<_> = gs - .events - .iter() - .filter_map(|e| match e { - ToSwarm::Dial { opts } => opts.get_peer_id(), - _ => None, - }) - .collect(); - - // Exactly config.prune_peers() many random peers should be dialled - assert_eq!(dials.len(), config.prune_peers()); - - let dials_set: HashSet<_> = dials.into_iter().collect(); - - // No duplicates - assert_eq!(dials_set.len(), config.prune_peers()); - - // all dial peers must be in px - assert!(dials_set.is_subset( - &px.iter() - .map(|i| *i.peer_id.as_ref().unwrap()) - .collect::>() - )); -} - -#[test] -fn test_send_px_and_backoff_in_prune() { - let config: Config = Config::default(); - - // build mesh with enough peers for px - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(config.prune_peers() + 1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - // send prune to peer - gs.send_graft_prune( - HashMap::new(), - vec![(peers[0], vec![topics[0].clone()])] - .into_iter() - .collect(), - HashSet::new(), - ); - - // check prune message - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] - && match m { - RpcOut::Prune(Prune { - topic_hash, - peers, - backoff, - }) => { - topic_hash == &topics[0] && - peers.len() == config.prune_peers() && - //all peers are different - peers.iter().collect::>().len() == - config.prune_peers() && - backoff.unwrap() == config.prune_backoff().as_secs() - } - _ => false, - } - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_prune_backoffed_peer_on_graft() { - let config: Config = Config::default(); - - // build mesh with enough peers for px - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(config.prune_peers() + 1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - // remove peer from mesh and send prune to peer => this adds a backoff for this peer - gs.mesh.get_mut(&topics[0]).unwrap().remove(&peers[0]); - gs.send_graft_prune( - HashMap::new(), - vec![(peers[0], vec![topics[0].clone()])] - .into_iter() - .collect(), - HashSet::new(), - ); - - // ignore all messages until now - let receivers = flush_events(&mut gs, receivers); - - // handle graft - gs.handle_graft(&peers[0], vec![topics[0].clone()]); - - // check prune message - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] - && match m { - RpcOut::Prune(Prune { - topic_hash, - peers, - backoff, - }) => { - topic_hash == &topics[0] && - //no px in this case - peers.is_empty() && - backoff.unwrap() == config.prune_backoff().as_secs() - } - _ => false, - } - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_do_not_graft_within_backoff_period() { - let config = ConfigBuilder::default() - .backoff_slack(1) - .heartbeat_interval(Duration::from_millis(100)) - .build() - .unwrap(); - // only one peer => mesh too small and will try to regraft as early as possible - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - // handle prune from peer with backoff of one second - gs.handle_prune(&peers[0], vec![(topics[0].clone(), Vec::new(), Some(1))]); - - // forget all events until now - let receivers = flush_events(&mut gs, receivers); - - // call heartbeat - gs.heartbeat(); - - // Sleep for one second and apply 10 regular heartbeats (interval = 100ms). - for _ in 0..10 { - sleep(Duration::from_millis(100)); - gs.heartbeat(); - } - - // Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat - // is needed). - let (control_msgs, receivers) = - count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert_eq!( - control_msgs, 0, - "Graft message created too early within backoff period" - ); - - // Heartbeat one more time this should graft now - sleep(Duration::from_millis(100)); - gs.heartbeat(); - - // check that graft got created - let (control_msgs, _) = count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert!( - control_msgs > 0, - "No graft message was created after backoff period" - ); -} - -#[test] -fn test_do_not_graft_within_default_backoff_period_after_receiving_prune_without_backoff() { - // set default backoff period to 1 second - let config = ConfigBuilder::default() - .prune_backoff(Duration::from_millis(90)) - .backoff_slack(1) - .heartbeat_interval(Duration::from_millis(100)) - .build() - .unwrap(); - // only one peer => mesh too small and will try to regraft as early as possible - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - // handle prune from peer without a specified backoff - gs.handle_prune(&peers[0], vec![(topics[0].clone(), Vec::new(), None)]); - - // forget all events until now - let receivers = flush_events(&mut gs, receivers); - - // call heartbeat - gs.heartbeat(); - - // Apply one more heartbeat - sleep(Duration::from_millis(100)); - gs.heartbeat(); - - // Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat - // is needed). - let (control_msgs, receivers) = - count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert_eq!( - control_msgs, 0, - "Graft message created too early within backoff period" - ); - - // Heartbeat one more time this should graft now - sleep(Duration::from_millis(100)); - gs.heartbeat(); - - // check that graft got created - let (control_msgs, _) = count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert!( - control_msgs > 0, - "No graft message was created after backoff period" - ); -} - -#[test] -fn test_unsubscribe_backoff() { - const HEARTBEAT_INTERVAL: Duration = Duration::from_millis(100); - let config = ConfigBuilder::default() - .backoff_slack(1) - // ensure a prune_backoff > unsubscribe_backoff - .prune_backoff(Duration::from_secs(5)) - .unsubscribe_backoff(1) - .heartbeat_interval(HEARTBEAT_INTERVAL) - .build() - .unwrap(); - - let topic = String::from("test"); - // only one peer => mesh too small and will try to regraft as early as possible - let (mut gs, _, receivers, topics) = inject_nodes1() - .peer_no(1) - .topics(vec![topic.clone()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - let _ = gs.unsubscribe(&Topic::new(topic)); - - let (control_msgs, receivers) = count_control_msgs(receivers, |_, m| match m { - RpcOut::Prune(Prune { backoff, .. }) => backoff == &Some(1), - _ => false, - }); - assert_eq!( - control_msgs, 1, - "Peer should be pruned with `unsubscribe_backoff`." - ); - - let _ = gs.subscribe(&Topic::new(topics[0].to_string())); - - // forget all events until now - let receivers = flush_events(&mut gs, receivers); - - // call heartbeat - gs.heartbeat(); - - // Sleep for one second and apply 10 regular heartbeats (interval = 100ms). - for _ in 0..10 { - sleep(HEARTBEAT_INTERVAL); - gs.heartbeat(); - } - - // Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat - // is needed). - let (control_msgs, receivers) = - count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert_eq!( - control_msgs, 0, - "Graft message created too early within backoff period" - ); - - // Heartbeat one more time this should graft now - sleep(HEARTBEAT_INTERVAL); - gs.heartbeat(); - - // check that graft got created - let (control_msgs, _) = count_control_msgs(receivers, |_, m| matches!(m, RpcOut::Graft { .. })); - assert!( - control_msgs > 0, - "No graft message was created after backoff period" - ); -} - -#[test] -fn test_flood_publish() { - let config: Config = Config::default(); - - let topic = "test"; - // Adds more peers than mesh can hold to test flood publishing - let (mut gs, _, receivers, _) = inject_nodes1() - .peer_no(config.mesh_n_high() + 10) - .topics(vec![topic.into()]) - .to_subscribe(true) - .create_network(); - - // publish message - let publish_data = vec![0; 42]; - gs.publish(Topic::new(topic), publish_data).unwrap(); - - // Collect all publish messages - let publishes = receivers - .into_values() - .fold(vec![], |mut collected_publish, c| { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { - collected_publish.push(message); - } - } - collected_publish - }); - - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform( - publishes - .first() - .expect("Should contain > 0 entries") - .clone(), - ) - .unwrap(); - - let msg_id = gs.config.message_id(message); - - let config: Config = Config::default(); - assert_eq!( - publishes.len(), - config.mesh_n_high() + 10, - "Should send a publish message to all known peers" - ); - - assert!( - gs.mcache.get(&msg_id).is_some(), - "Message cache should contain published message" - ); -} - -#[test] -fn test_gossip_to_at_least_gossip_lazy_peers() { - let config: Config = Config::default(); - - // add more peers than in mesh to test gossipping - // by default only mesh_n_low peers will get added to mesh - let (mut gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(config.mesh_n_low() + config.gossip_lazy() + 1) - .topics(vec!["topic".into()]) - .to_subscribe(true) - .create_network(); - - // receive message - let raw_message = RawMessage { - source: Some(PeerId::random()), - data: vec![], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(raw_message.clone(), &PeerId::random()); - - // emit gossip - gs.emit_gossip(); - - // Transform the inbound message - let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); - - let msg_id = gs.config.message_id(message); - - // check that exactly config.gossip_lazy() many gossip messages were sent. - let (control_msgs, _) = count_control_msgs(receivers, |_, action| match action { - RpcOut::IHave(IHave { - topic_hash, - message_ids, - }) => topic_hash == &topic_hashes[0] && message_ids.iter().any(|id| id == &msg_id), - _ => false, - }); - assert_eq!(control_msgs, config.gossip_lazy()); -} - -#[test] -fn test_gossip_to_at_most_gossip_factor_peers() { - let config: Config = Config::default(); - - // add a lot of peers - let m = config.mesh_n_low() + config.gossip_lazy() * (2.0 / config.gossip_factor()) as usize; - let (mut gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(m) - .topics(vec!["topic".into()]) - .to_subscribe(true) - .create_network(); - - // receive message - let raw_message = RawMessage { - source: Some(PeerId::random()), - data: vec![], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(raw_message.clone(), &PeerId::random()); - - // emit gossip - gs.emit_gossip(); - - // Transform the inbound message - let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); - - let msg_id = gs.config.message_id(message); - // check that exactly config.gossip_lazy() many gossip messages were sent. - let (control_msgs, _) = count_control_msgs(receivers, |_, action| match action { - RpcOut::IHave(IHave { - topic_hash, - message_ids, - }) => topic_hash == &topic_hashes[0] && message_ids.iter().any(|id| id == &msg_id), - _ => false, - }); - assert_eq!( - control_msgs, - ((m - config.mesh_n_low()) as f64 * config.gossip_factor()) as usize - ); -} - -#[test] -fn test_accept_only_outbound_peer_grafts_when_mesh_full() { - let config: Config = Config::default(); - - // enough peers to fill the mesh - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - // graft all the peers => this will fill the mesh - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // assert current mesh size - assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n_high()); - - // create an outbound and an inbound peer - let (inbound, _in_receiver) = add_peer(&mut gs, &topics, false, false); - let (outbound, _out_receiver) = add_peer(&mut gs, &topics, true, false); - - // send grafts - gs.handle_graft(&inbound, vec![topics[0].clone()]); - gs.handle_graft(&outbound, vec![topics[0].clone()]); - - // assert mesh size - assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n_high() + 1); - - // inbound is not in mesh - assert!(!gs.mesh[&topics[0]].contains(&inbound)); - - // outbound is in mesh - assert!(gs.mesh[&topics[0]].contains(&outbound)); -} - -#[test] -fn test_do_not_remove_too_many_outbound_peers() { - // use an extreme case to catch errors with high probability - let m = 50; - let n = 2 * m; - let config = ConfigBuilder::default() - .mesh_n_high(n) - .mesh_n(n) - .mesh_n_low(n) - .mesh_outbound_min(m) - .build() - .unwrap(); - - // fill the mesh with inbound connections - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(n) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - // graft all the peers - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // create m outbound connections and graft (we will accept the graft) - let mut outbound = HashSet::new(); - for _ in 0..m { - let (peer, _) = add_peer(&mut gs, &topics, true, false); - outbound.insert(peer); - gs.handle_graft(&peer, topics.clone()); - } - - // mesh is overly full - assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), n + m); - - // run a heartbeat - gs.heartbeat(); - - // Peers should be removed to reach n - assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), n); - - // all outbound peers are still in the mesh - assert!(outbound.iter().all(|p| gs.mesh[&topics[0]].contains(p))); -} - -#[test] -fn test_add_outbound_peers_if_min_is_not_satisfied() { - let config: Config = Config::default(); - - // Fill full mesh with inbound peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - // graft all the peers - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // create config.mesh_outbound_min() many outbound connections without grafting - let mut peers = vec![]; - for _ in 0..config.mesh_outbound_min() { - peers.push(add_peer(&mut gs, &topics, true, false)); - } - - // Nothing changed in the mesh yet - assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n_high()); - - // run a heartbeat - gs.heartbeat(); - - // The outbound peers got additionally added - assert_eq!( - gs.mesh[&topics[0]].len(), - config.mesh_n_high() + config.mesh_outbound_min() - ); -} - -#[test] -fn test_prune_negative_scored_peers() { - let config = Config::default(); - - // build mesh with one peer - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - // add penalty to peer - gs.as_peer_score_mut().add_penalty(&peers[0], 1); - - // execute heartbeat - gs.heartbeat(); - - // peer should not be in mesh anymore - assert!(gs.mesh[&topics[0]].is_empty()); - - // check prune message - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[0] - && match m { - RpcOut::Prune(Prune { - topic_hash, - peers, - backoff, - }) => { - topic_hash == &topics[0] && - //no px in this case - peers.is_empty() && - backoff.unwrap() == config.prune_backoff().as_secs() - } - _ => false, - } - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_dont_graft_to_negative_scored_peers() { - let config = Config::default(); - // init full mesh - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - // add two additional peers that will not be part of the mesh - let (p1, _receiver1) = add_peer(&mut gs, &topics, false, false); - let (p2, _receiver2) = add_peer(&mut gs, &topics, false, false); - - // reduce score of p1 to negative - gs.as_peer_score_mut().add_penalty(&p1, 1); - - // handle prunes of all other peers - for p in peers { - gs.handle_prune(&p, vec![(topics[0].clone(), Vec::new(), None)]); - } - - // heartbeat - gs.heartbeat(); - - // assert that mesh only contains p2 - assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), 1); - assert!(gs.mesh.get(&topics[0]).unwrap().contains(&p2)); -} - -/// Note that in this test also without a penalty the px would be ignored because of the -/// acceptPXThreshold, but the spec still explicitly states the rule that px from negative -/// peers should get ignored, therefore we test it here. -#[test] -fn test_ignore_px_from_negative_scored_peer() { - let config = Config::default(); - - // build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - // penalize peer - gs.as_peer_score_mut().add_penalty(&peers[0], 1); - - // handle prune from single peer with px peers - let px = vec![PeerInfo { - peer_id: Some(PeerId::random()), - }]; - - gs.handle_prune( - &peers[0], - vec![( - topics[0].clone(), - px, - Some(config.prune_backoff().as_secs()), - )], - ); - - // assert no dials - assert_eq!( - gs.events - .iter() - .filter(|e| matches!(e, ToSwarm::Dial { .. })) - .count(), - 0 - ); -} - -#[test] -fn test_only_send_nonnegative_scoring_peers_in_px() { - let config = ConfigBuilder::default() - .prune_peers(16) - .do_px() - .build() - .unwrap(); - - // Build mesh with three peer - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(3) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - // Penalize first peer - gs.as_peer_score_mut().add_penalty(&peers[0], 1); - - // Prune second peer - gs.send_graft_prune( - HashMap::new(), - vec![(peers[1], vec![topics[0].clone()])] - .into_iter() - .collect(), - HashSet::new(), - ); - - // Check that px in prune message only contains third peer - let (control_msgs, _) = count_control_msgs(receivers, |peer_id, m| { - peer_id == &peers[1] - && match m { - RpcOut::Prune(Prune { - topic_hash, - peers: px, - .. - }) => { - topic_hash == &topics[0] - && px.len() == 1 - && px[0].peer_id.as_ref().unwrap() == &peers[2] - } - _ => false, - } - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_do_not_gossip_to_peers_below_gossip_threshold() { - let config = Config::default(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - - // Build full mesh - let (mut gs, peers, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // Graft all the peer - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // Add two additional peers that will not be part of the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - // Reduce score of p1 below peer_score_thresholds.gossip_threshold - // note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.as_peer_score_mut().add_penalty(&p1, 2); - - // Reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold - gs.as_peer_score_mut().add_penalty(&p2, 1); - - // Receive message - let raw_message = RawMessage { - source: Some(PeerId::random()), - data: vec![], - sequence_number: Some(0), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(raw_message.clone(), &PeerId::random()); - - // Transform the inbound message - let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); - - let msg_id = gs.config.message_id(message); - - // Emit gossip - gs.emit_gossip(); - - // Check that exactly one gossip messages got sent and it got sent to p2 - let (control_msgs, _) = count_control_msgs(receivers, |peer, action| match action { - RpcOut::IHave(IHave { - topic_hash, - message_ids, - }) => { - if topic_hash == &topics[0] && message_ids.iter().any(|id| id == &msg_id) { - assert_eq!(peer, &p2); - true - } else { - false - } - } - _ => false, - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_iwant_msg_from_peer_below_gossip_threshold_gets_ignored() { - let config = Config::default(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - - // Build full mesh - let (mut gs, peers, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // Graft all the peer - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // Add two additional peers that will not be part of the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - // Reduce score of p1 below peer_score_thresholds.gossip_threshold - // note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.as_peer_score_mut().add_penalty(&p1, 2); - - // Reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold - gs.as_peer_score_mut().add_penalty(&p2, 1); - - // Receive message - let raw_message = RawMessage { - source: Some(PeerId::random()), - data: vec![], - sequence_number: Some(0), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(raw_message.clone(), &PeerId::random()); - - // Transform the inbound message - let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); - - let msg_id = gs.config.message_id(message); - - gs.handle_iwant(&p1, vec![msg_id.clone()]); - gs.handle_iwant(&p2, vec![msg_id.clone()]); - - // the messages we are sending - let sent_messages = - receivers - .into_iter() - .fold(vec![], |mut collected_messages, (peer_id, c)| { - let non_priority = c.non_priority.get_ref(); - while !non_priority.is_empty() { - if let Ok(RpcOut::Forward { message, .. }) = non_priority.try_recv() { - collected_messages.push((peer_id, message)); - } - } - collected_messages - }); - - // the message got sent to p2 - assert!(sent_messages - .iter() - .map(|(peer_id, msg)| ( - peer_id, - gs.data_transform.inbound_transform(msg.clone()).unwrap() - )) - .any(|(peer_id, msg)| peer_id == &p2 && gs.config.message_id(&msg) == msg_id)); - // the message got not sent to p1 - assert!(sent_messages - .iter() - .map(|(peer_id, msg)| ( - peer_id, - gs.data_transform.inbound_transform(msg.clone()).unwrap() - )) - .all(|(peer_id, msg)| !(peer_id == &p1 && gs.config.message_id(&msg) == msg_id))); -} - -#[test] -fn test_ihave_msg_from_peer_below_gossip_threshold_gets_ignored() { - let config = Config::default(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - // build full mesh - let (mut gs, peers, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // graft all the peer - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // add two additional peers that will not be part of the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - // reduce score of p1 below peer_score_thresholds.gossip_threshold - // note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.as_peer_score_mut().add_penalty(&p1, 2); - - // reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold - gs.as_peer_score_mut().add_penalty(&p2, 1); - - // message that other peers have - let raw_message = RawMessage { - source: Some(PeerId::random()), - data: vec![], - sequence_number: Some(0), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - - // Transform the inbound message - let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); - - let msg_id = gs.config.message_id(message); - - gs.handle_ihave(&p1, vec![(topics[0].clone(), vec![msg_id.clone()])]); - gs.handle_ihave(&p2, vec![(topics[0].clone(), vec![msg_id.clone()])]); - - // check that we sent exactly one IWANT request to p2 - let (control_msgs, _) = count_control_msgs(receivers, |peer, c| match c { - RpcOut::IWant(IWant { message_ids }) => { - if message_ids.iter().any(|m| m == &msg_id) { - assert_eq!(peer, &p2); - true - } else { - false - } - } - _ => false, - }); - assert_eq!(control_msgs, 1); -} - -#[test] -fn test_do_not_publish_to_peer_below_publish_threshold() { - let config = ConfigBuilder::default() - .flood_publish(false) - .build() - .unwrap(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, - publish_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - - // build mesh with no peers and no subscribed topics - let (mut gs, _, mut receivers, _) = inject_nodes1() - .gs_config(config) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // create a new topic for which we are not subscribed - let topic = Topic::new("test"); - let topics = vec![topic.hash()]; - - // add two additional peers that will be added to the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - // reduce score of p1 below peer_score_thresholds.publish_threshold - // note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.as_peer_score_mut().add_penalty(&p1, 2); - - // reduce score of p2 below 0 but not below peer_score_thresholds.publish_threshold - gs.as_peer_score_mut().add_penalty(&p2, 1); - - // a heartbeat will remove the peers from the mesh - gs.heartbeat(); - - // publish on topic - let publish_data = vec![0; 42]; - gs.publish(topic, publish_data).unwrap(); - - // Collect all publish messages - let publishes = receivers - .into_iter() - .fold(vec![], |mut collected_publish, (peer_id, c)| { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { - collected_publish.push((peer_id, message)); - } - } - collected_publish - }); - - // assert only published to p2 - assert_eq!(publishes.len(), 1); - assert_eq!(publishes[0].0, p2); -} - -#[test] -fn test_do_not_flood_publish_to_peer_below_publish_threshold() { - let config = Config::default(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, - publish_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - // build mesh with no peers - let (mut gs, _, mut receivers, topics) = inject_nodes1() - .topics(vec!["test".into()]) - .gs_config(config) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // add two additional peers that will be added to the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - // reduce score of p1 below peer_score_thresholds.publish_threshold - // note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.as_peer_score_mut().add_penalty(&p1, 2); - - // reduce score of p2 below 0 but not below peer_score_thresholds.publish_threshold - gs.as_peer_score_mut().add_penalty(&p2, 1); - - // a heartbeat will remove the peers from the mesh - gs.heartbeat(); - - // publish on topic - let publish_data = vec![0; 42]; - gs.publish(Topic::new("test"), publish_data).unwrap(); - - // Collect all publish messages - let publishes = receivers - .into_iter() - .fold(vec![], |mut collected_publish, (peer_id, c)| { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { - collected_publish.push((peer_id, message)) - } - } - collected_publish - }); - - // assert only published to p2 - assert_eq!(publishes.len(), 1); - assert!(publishes[0].0 == p2); -} - -#[test] -fn test_ignore_rpc_from_peers_below_graylist_threshold() { - let config = Config::default(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, - publish_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, - graylist_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, - ..PeerScoreThresholds::default() - }; - - // build mesh with no peers - let (mut gs, _, _, topics) = inject_nodes1() - .topics(vec!["test".into()]) - .gs_config(config.clone()) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // add two additional peers that will be added to the mesh - let (p1, _receiver1) = add_peer(&mut gs, &topics, false, false); - let (p2, _receiver2) = add_peer(&mut gs, &topics, false, false); - - // reduce score of p1 below peer_score_thresholds.graylist_threshold - // note that penalties get squared so two penalties means a score of - // 4 * peer_score_params.behaviour_penalty_weight. - gs.as_peer_score_mut().add_penalty(&p1, 2); - - // reduce score of p2 below publish_threshold but not below graylist_threshold - gs.as_peer_score_mut().add_penalty(&p2, 1); - - let raw_message1 = RawMessage { - source: Some(PeerId::random()), - data: vec![1, 2, 3, 4], - sequence_number: Some(1u64), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - - let raw_message2 = RawMessage { - source: Some(PeerId::random()), - data: vec![1, 2, 3, 4, 5], - sequence_number: Some(2u64), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - - let raw_message3 = RawMessage { - source: Some(PeerId::random()), - data: vec![1, 2, 3, 4, 5, 6], - sequence_number: Some(3u64), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - - let raw_message4 = RawMessage { - source: Some(PeerId::random()), - data: vec![1, 2, 3, 4, 5, 6, 7], - sequence_number: Some(4u64), - topic: topics[0].clone(), - signature: None, - key: None, - validated: true, - }; - - // Transform the inbound message - let message2 = &gs.data_transform.inbound_transform(raw_message2).unwrap(); - - // Transform the inbound message - let message4 = &gs.data_transform.inbound_transform(raw_message4).unwrap(); - - let subscription = Subscription { - action: SubscriptionAction::Subscribe, - topic_hash: topics[0].clone(), - }; - - let control_action = ControlAction::IHave(IHave { - topic_hash: topics[0].clone(), - message_ids: vec![config.message_id(message2)], - }); - - // clear events - gs.events.clear(); - - // receive from p1 - gs.on_connection_handler_event( - p1, - ConnectionId::new_unchecked(0), - HandlerEvent::Message { - rpc: Rpc { - messages: vec![raw_message1], - subscriptions: vec![subscription.clone()], - control_msgs: vec![control_action], - }, - invalid_messages: Vec::new(), - }, - ); - - // only the subscription event gets processed, the rest is dropped - assert_eq!(gs.events.len(), 1); - assert!(matches!( - gs.events[0], - ToSwarm::GenerateEvent(Event::Subscribed { .. }) - )); - - let control_action = ControlAction::IHave(IHave { - topic_hash: topics[0].clone(), - message_ids: vec![config.message_id(message4)], - }); - - // receive from p2 - gs.on_connection_handler_event( - p2, - ConnectionId::new_unchecked(0), - HandlerEvent::Message { - rpc: Rpc { - messages: vec![raw_message3], - subscriptions: vec![subscription], - control_msgs: vec![control_action], - }, - invalid_messages: Vec::new(), - }, - ); - - // events got processed - assert!(gs.events.len() > 1); -} - -#[test] -fn test_ignore_px_from_peers_below_accept_px_threshold() { - let config = ConfigBuilder::default().prune_peers(16).build().unwrap(); - let peer_score_params = PeerScoreParams::default(); - let peer_score_thresholds = PeerScoreThresholds { - accept_px_threshold: peer_score_params.app_specific_weight, - ..PeerScoreThresholds::default() - }; - // Build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // Decrease score of first peer to less than accept_px_threshold - gs.set_application_score(&peers[0], 0.99); - - // Increase score of second peer to accept_px_threshold - gs.set_application_score(&peers[1], 1.0); - - // Handle prune from peer peers[0] with px peers - let px = vec![PeerInfo { - peer_id: Some(PeerId::random()), - }]; - gs.handle_prune( - &peers[0], - vec![( - topics[0].clone(), - px, - Some(config.prune_backoff().as_secs()), - )], - ); - - // Assert no dials - assert_eq!( - gs.events - .iter() - .filter(|e| matches!(e, ToSwarm::Dial { .. })) - .count(), - 0 - ); - - // handle prune from peer peers[1] with px peers - let px = vec![PeerInfo { - peer_id: Some(PeerId::random()), - }]; - gs.handle_prune( - &peers[1], - vec![( - topics[0].clone(), - px, - Some(config.prune_backoff().as_secs()), - )], - ); - - // assert there are dials now - assert!( - gs.events - .iter() - .filter(|e| matches!(e, ToSwarm::Dial { .. })) - .count() - > 0 - ); -} - -#[test] -fn test_keep_best_scoring_peers_on_oversubscription() { - let config = ConfigBuilder::default() - .mesh_n_low(15) - .mesh_n(30) - .mesh_n_high(60) - .retain_scores(29) - .build() - .unwrap(); - - // build mesh with more peers than mesh can hold - let n = config.mesh_n_high() + 1; - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(n) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(n) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - // graft all, will be accepted since the are outbound - for peer in &peers { - gs.handle_graft(peer, topics.clone()); - } - - // assign scores to peers equalling their index - - // set random positive scores - for (index, peer) in peers.iter().enumerate() { - gs.set_application_score(peer, index as f64); - } - - assert_eq!(gs.mesh[&topics[0]].len(), n); - - // heartbeat to prune some peers - gs.heartbeat(); - - assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n()); - - // mesh contains retain_scores best peers - assert!(gs.mesh[&topics[0]].is_superset( - &peers[(n - config.retain_scores())..] - .iter() - .cloned() - .collect() - )); -} - -#[test] -fn test_scoring_p1() { - let config = Config::default(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 2.0, - time_in_mesh_quantum: Duration::from_millis(50), - time_in_mesh_cap: 10.0, - topic_weight: 0.7, - ..TopicScoreParams::default() - }; - peer_score_params - .topics - .insert(topic_hash, topic_params.clone()); - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with one peer - let (mut gs, peers, _, _) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - // sleep for 2 times the mesh_quantum - sleep(topic_params.time_in_mesh_quantum * 2); - // refresh scores - gs.as_peer_score_mut().refresh_scores(); - assert!( - gs.as_peer_score_mut().score_report(&peers[0]).score - >= 2.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, - "score should be at least 2 * time_in_mesh_weight * topic_weight" - ); - assert!( - gs.as_peer_score_mut().score_report(&peers[0]).score - < 3.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, - "score should be less than 3 * time_in_mesh_weight * topic_weight" - ); - - // sleep again for 2 times the mesh_quantum - sleep(topic_params.time_in_mesh_quantum * 2); - // refresh scores - gs.as_peer_score_mut().refresh_scores(); - assert!( - gs.as_peer_score_mut().score_report(&peers[0]).score - >= 2.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, - "score should be at least 4 * time_in_mesh_weight * topic_weight" - ); - - // sleep for enough periods to reach maximum - sleep(topic_params.time_in_mesh_quantum * (topic_params.time_in_mesh_cap - 3.0) as u32); - // refresh scores - gs.as_peer_score_mut().refresh_scores(); - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - topic_params.time_in_mesh_cap - * topic_params.time_in_mesh_weight - * topic_params.topic_weight, - "score should be exactly time_in_mesh_cap * time_in_mesh_weight * topic_weight" - ); -} - -fn random_message(seq: &mut u64, topics: &[TopicHash]) -> RawMessage { - let mut rng = rand::thread_rng(); - *seq += 1; - RawMessage { - source: Some(PeerId::random()), - data: (0..rng.gen_range(10..30)).map(|_| rng.gen()).collect(), - sequence_number: Some(*seq), - topic: topics[rng.gen_range(0..topics.len())].clone(), - signature: None, - key: None, - validated: true, - } -} - -#[test] -fn test_scoring_p2() { - let config = Config::default(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, // deactivate time in mesh - first_message_deliveries_weight: 2.0, - first_message_deliveries_cap: 10.0, - first_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..TopicScoreParams::default() - }; - peer_score_params - .topics - .insert(topic_hash, topic_params.clone()); - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - let m1 = random_message(&mut seq, &topics); - // peer 0 delivers message first - deliver_message(&mut gs, 0, m1.clone()); - // peer 1 delivers message second - deliver_message(&mut gs, 1, m1); - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - 1.0 * topic_params.first_message_deliveries_weight * topic_params.topic_weight, - "score should be exactly first_message_deliveries_weight * topic_weight" - ); - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[1]).score, - 0.0, - "there should be no score for second message deliveries * topic_weight" - ); - - // peer 2 delivers two new messages - deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); - deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[1]).score, - 2.0 * topic_params.first_message_deliveries_weight * topic_params.topic_weight, - "score should be exactly 2 * first_message_deliveries_weight * topic_weight" - ); - - // test decaying - gs.as_peer_score_mut().refresh_scores(); - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - 1.0 * topic_params.first_message_deliveries_decay - * topic_params.first_message_deliveries_weight - * topic_params.topic_weight, - "score should be exactly first_message_deliveries_decay * \ - first_message_deliveries_weight * topic_weight" - ); - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[1]).score, - 2.0 * topic_params.first_message_deliveries_decay - * topic_params.first_message_deliveries_weight - * topic_params.topic_weight, - "score should be exactly 2 * first_message_deliveries_decay * \ - first_message_deliveries_weight * topic_weight" - ); - - // test cap - for _ in 0..topic_params.first_message_deliveries_cap as u64 { - deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); - } - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[1]).score, - topic_params.first_message_deliveries_cap - * topic_params.first_message_deliveries_weight - * topic_params.topic_weight, - "score should be exactly first_message_deliveries_cap * \ - first_message_deliveries_weight * topic_weight" - ); -} - -#[test] -fn test_scoring_p3() { - let config = Config::default(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, // deactivate time in mesh - first_message_deliveries_weight: 0.0, // deactivate first time deliveries - mesh_message_deliveries_weight: -2.0, - mesh_message_deliveries_decay: 0.9, - mesh_message_deliveries_cap: 10.0, - mesh_message_deliveries_threshold: 5.0, - mesh_message_deliveries_activation: Duration::from_secs(1), - mesh_message_deliveries_window: Duration::from_millis(100), - topic_weight: 0.7, - ..TopicScoreParams::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - let mut expected_message_deliveries = 0.0; - - // messages used to test window - let m1 = random_message(&mut seq, &topics); - let m2 = random_message(&mut seq, &topics); - - // peer 1 delivers m1 - deliver_message(&mut gs, 1, m1.clone()); - - // peer 0 delivers two message - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - expected_message_deliveries += 2.0; - - sleep(Duration::from_millis(60)); - - // peer 1 delivers m2 - deliver_message(&mut gs, 1, m2.clone()); - - sleep(Duration::from_millis(70)); - // peer 0 delivers m1 and m2 only m2 gets counted - deliver_message(&mut gs, 0, m1); - deliver_message(&mut gs, 0, m2); - expected_message_deliveries += 1.0; - - sleep(Duration::from_millis(900)); - - // message deliveries penalties get activated, peer 0 has only delivered 3 messages and - // therefore gets a penalty - gs.as_peer_score_mut().refresh_scores(); - expected_message_deliveries *= 0.9; // decay - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - (5f64 - expected_message_deliveries).powi(2) * -2.0 * 0.7 - ); - - // peer 0 delivers a lot of messages => message_deliveries should be capped at 10 - for _ in 0..20 { - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - } - - expected_message_deliveries = 10.0; - - assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); - - // apply 10 decays - for _ in 0..10 { - gs.as_peer_score_mut().refresh_scores(); - expected_message_deliveries *= 0.9; // decay - } - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - (5f64 - expected_message_deliveries).powi(2) * -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p3b() { - let config = ConfigBuilder::default() - .prune_backoff(Duration::from_millis(100)) - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - time_in_mesh_weight: 0.0, // deactivate time in mesh - first_message_deliveries_weight: 0.0, // deactivate first time deliveries - mesh_message_deliveries_weight: -2.0, - mesh_message_deliveries_decay: 0.9, - mesh_message_deliveries_cap: 10.0, - mesh_message_deliveries_threshold: 5.0, - mesh_message_deliveries_activation: Duration::from_secs(1), - mesh_message_deliveries_window: Duration::from_millis(100), - mesh_failure_penalty_weight: -3.0, - mesh_failure_penalty_decay: 0.95, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - let mut expected_message_deliveries = 0.0; - - // add some positive score - gs.as_peer_score_mut() - .set_application_score(&peers[0], 100.0); - - // peer 0 delivers two message - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - expected_message_deliveries += 2.0; - - sleep(Duration::from_millis(1050)); - - // activation kicks in - gs.as_peer_score_mut().refresh_scores(); - expected_message_deliveries *= 0.9; // decay - - // prune peer - gs.handle_prune(&peers[0], vec![(topics[0].clone(), vec![], None)]); - - // wait backoff - sleep(Duration::from_millis(130)); - - // regraft peer - gs.handle_graft(&peers[0], topics.clone()); - - // the score should now consider p3b - let mut expected_b3 = (5f64 - expected_message_deliveries).powi(2); - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - 100.0 + expected_b3 * -3.0 * 0.7 - ); - - // we can also add a new p3 to the score - - // peer 0 delivers one message - deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); - expected_message_deliveries += 1.0; - - sleep(Duration::from_millis(1050)); - gs.as_peer_score_mut().refresh_scores(); - expected_message_deliveries *= 0.9; // decay - expected_b3 *= 0.95; - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - 100.0 + (expected_b3 * -3.0 + (5f64 - expected_message_deliveries).powi(2) * -2.0) * 0.7 - ); -} - -#[test] -fn test_scoring_p4_valid_message() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - // deactivate time in mesh - time_in_mesh_weight: 0.0, - // deactivate first time deliveries - first_message_deliveries_weight: 0.0, - // deactivate message deliveries - mesh_message_deliveries_weight: 0.0, - // deactivate mesh failure penalties - mesh_failure_penalty_weight: 0.0, - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - // peer 0 delivers valid message - let m1 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); - - assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); - - // message m1 gets validated - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Accept, - ); - - assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); -} - -#[test] -fn test_scoring_p4_invalid_signature() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - // deactivate time in mesh - time_in_mesh_weight: 0.0, - // deactivate first time deliveries - first_message_deliveries_weight: 0.0, - // deactivate message deliveries - mesh_message_deliveries_weight: 0.0, - // deactivate mesh failure penalties - mesh_failure_penalty_weight: 0.0, - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - - // peer 0 delivers message with invalid signature - let m = random_message(&mut seq, &topics); - - gs.on_connection_handler_event( - peers[0], - ConnectionId::new_unchecked(0), - HandlerEvent::Message { - rpc: Rpc { - messages: vec![], - subscriptions: vec![], - control_msgs: vec![], - }, - invalid_messages: vec![(m, ValidationError::InvalidSignature)], - }, - ); - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p4_message_from_self() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - // deactivate time in mesh - time_in_mesh_weight: 0.0, - // deactivate first time deliveries - first_message_deliveries_weight: 0.0, - // deactivate message deliveries - mesh_message_deliveries_weight: 0.0, - // deactivate mesh failure penalties - mesh_failure_penalty_weight: 0.0, - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - // peer 0 delivers invalid message from self - let mut m = random_message(&mut seq, &topics); - m.source = Some(*gs.publish_config.get_own_id().unwrap()); - - deliver_message(&mut gs, 0, m); - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p4_ignored_message() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - // deactivate time in mesh - time_in_mesh_weight: 0.0, - // deactivate first time deliveries - first_message_deliveries_weight: 0.0, - // deactivate message deliveries - mesh_message_deliveries_weight: 0.0, - // deactivate mesh failure penalties - mesh_failure_penalty_weight: 0.0, - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - // peer 0 delivers ignored message - let m1 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - - assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); - - // message m1 gets ignored - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Ignore, - ); - - assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); -} - -#[test] -fn test_scoring_p4_application_invalidated_message() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - // deactivate time in mesh - time_in_mesh_weight: 0.0, - // deactivate first time deliveries - first_message_deliveries_weight: 0.0, - // deactivate message deliveries - mesh_message_deliveries_weight: 0.0, - // deactivate mesh failure penalties - mesh_failure_penalty_weight: 0.0, - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - // peer 0 delivers invalid message - let m1 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - - assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); - - // message m1 gets rejected - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Reject, - ); - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p4_application_invalid_message_from_two_peers() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - // deactivate time in mesh - time_in_mesh_weight: 0.0, - // deactivate first time deliveries - first_message_deliveries_weight: 0.0, - // deactivate message deliveries - mesh_message_deliveries_weight: 0.0, - // deactivate mesh failure penalties - mesh_failure_penalty_weight: 0.0, - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with two peers - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - // peer 0 delivers invalid message - let m1 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1.clone()).unwrap(); - - // peer 1 delivers same message - deliver_message(&mut gs, 1, m1); - - assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); - assert_eq!(gs.as_peer_score_mut().score_report(&peers[1]).score, 0.0); - - // message m1 gets rejected - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Reject, - ); - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - -2.0 * 0.7 - ); - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[1]).score, - -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p4_three_application_invalid_messages() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - // deactivate time in mesh - time_in_mesh_weight: 0.0, - // deactivate first time deliveries - first_message_deliveries_weight: 0.0, - // deactivate message deliveries - mesh_message_deliveries_weight: 0.0, - // deactivate mesh failure penalties - mesh_failure_penalty_weight: 0.0, - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - // peer 0 delivers two invalid message - let m1 = random_message(&mut seq, &topics); - let m2 = random_message(&mut seq, &topics); - let m3 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - deliver_message(&mut gs, 0, m2.clone()); - deliver_message(&mut gs, 0, m3.clone()); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); - - // Transform the inbound message - let message2 = &gs.data_transform.inbound_transform(m2).unwrap(); - // Transform the inbound message - let message3 = &gs.data_transform.inbound_transform(m3).unwrap(); - - assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); - - // messages gets rejected - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Reject, - ); - - gs.report_message_validation_result( - &config.message_id(message2), - &peers[0], - MessageAcceptance::Reject, - ); - - gs.report_message_validation_result( - &config.message_id(message3), - &peers[0], - MessageAcceptance::Reject, - ); - - // number of invalid messages gets squared - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - 9.0 * -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p4_decay() { - let config = ConfigBuilder::default() - .validate_messages() - .build() - .unwrap(); - let mut peer_score_params = PeerScoreParams::default(); - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let topic_params = TopicScoreParams { - // deactivate time in mesh - time_in_mesh_weight: 0.0, - // deactivate first time deliveries - first_message_deliveries_weight: 0.0, - // deactivate message deliveries - mesh_message_deliveries_weight: 0.0, - // deactivate mesh failure penalties - mesh_failure_penalty_weight: 0.0, - invalid_message_deliveries_weight: -2.0, - invalid_message_deliveries_decay: 0.9, - topic_weight: 0.7, - ..Default::default() - }; - peer_score_params.topics.insert(topic_hash, topic_params); - peer_score_params.app_specific_weight = 1.0; - let peer_score_thresholds = PeerScoreThresholds::default(); - - // build mesh with one peer - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, peer_score_thresholds))) - .create_network(); - - let mut seq = 0; - let deliver_message = |gs: &mut Behaviour, index: usize, msg: RawMessage| { - gs.handle_received_message(msg, &peers[index]); - }; - - // peer 0 delivers invalid message - let m1 = random_message(&mut seq, &topics); - deliver_message(&mut gs, 0, m1.clone()); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); - assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); - - // message m1 gets rejected - gs.report_message_validation_result( - &config.message_id(message1), - &peers[0], - MessageAcceptance::Reject, - ); - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - -2.0 * 0.7 - ); - - // we decay - gs.as_peer_score_mut().refresh_scores(); - - // the number of invalids gets decayed to 0.9 and then squared in the score - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - 0.9 * 0.9 * -2.0 * 0.7 - ); -} - -#[test] -fn test_scoring_p5() { - let peer_score_params = PeerScoreParams { - app_specific_weight: 2.0, - ..PeerScoreParams::default() - }; - - // build mesh with one peer - let (mut gs, peers, _, _) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) - .create_network(); - - gs.set_application_score(&peers[0], 1.1); - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - 1.1 * 2.0 - ); -} - -#[test] -fn test_scoring_p6() { - let peer_score_params = PeerScoreParams { - ip_colocation_factor_threshold: 5.0, - ip_colocation_factor_weight: -2.0, - ..Default::default() - }; - - let (mut gs, _, _, _) = inject_nodes1() - .peer_no(0) - .topics(vec![]) - .to_subscribe(false) - .gs_config(Config::default()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) - .create_network(); - - // create 5 peers with the same ip - let addr = Multiaddr::from(Ipv4Addr::new(10, 1, 2, 3)); - let peers = vec![ - add_peer_with_addr(&mut gs, &[], false, false, addr.clone()).0, - add_peer_with_addr(&mut gs, &[], false, false, addr.clone()).0, - add_peer_with_addr(&mut gs, &[], true, false, addr.clone()).0, - add_peer_with_addr(&mut gs, &[], true, false, addr.clone()).0, - add_peer_with_addr(&mut gs, &[], true, true, addr.clone()).0, - ]; - - // create 4 other peers with other ip - let addr2 = Multiaddr::from(Ipv4Addr::new(10, 1, 2, 4)); - let others = vec![ - add_peer_with_addr(&mut gs, &[], false, false, addr2.clone()).0, - add_peer_with_addr(&mut gs, &[], false, false, addr2.clone()).0, - add_peer_with_addr(&mut gs, &[], true, false, addr2.clone()).0, - add_peer_with_addr(&mut gs, &[], true, false, addr2.clone()).0, - ]; - - // no penalties yet - for peer in peers.iter().chain(others.iter()) { - assert_eq!(gs.as_peer_score_mut().score_report(peer).score, 0.0); - } - - // add additional connection for 3 others with addr - for id in others.iter().take(3) { - gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { - peer_id: *id, - connection_id: ConnectionId::new_unchecked(0), - endpoint: &ConnectedPoint::Dialer { - address: addr.clone(), - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - }, - failed_addresses: &[], - other_established: 0, - })); - } - - // penalties apply squared - for peer in peers.iter().chain(others.iter().take(3)) { - assert_eq!(gs.as_peer_score_mut().score_report(peer).score, 9.0 * -2.0); - } - // fourth other peer still no penalty - assert_eq!(gs.as_peer_score_mut().score_report(&others[3]).score, 0.0); - - // add additional connection for 3 of the peers to addr2 - for peer in peers.iter().take(3) { - gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { - peer_id: *peer, - connection_id: ConnectionId::new_unchecked(0), - endpoint: &ConnectedPoint::Dialer { - address: addr2.clone(), - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - }, - failed_addresses: &[], - other_established: 1, - })); - } - - // double penalties for the first three of each - for peer in peers.iter().take(3).chain(others.iter().take(3)) { - assert_eq!( - gs.as_peer_score_mut().score_report(peer).score, - (9.0 + 4.0) * -2.0 - ); - } - - // single penalties for the rest - for peer in peers.iter().skip(3) { - assert_eq!(gs.as_peer_score_mut().score_report(peer).score, 9.0 * -2.0); - } - assert_eq!( - gs.as_peer_score_mut().score_report(&others[3]).score, - 4.0 * -2.0 - ); - - // two times same ip doesn't count twice - gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { - peer_id: peers[0], - connection_id: ConnectionId::new_unchecked(0), - endpoint: &ConnectedPoint::Dialer { - address: addr, - role_override: Endpoint::Dialer, - port_use: PortUse::Reuse, - }, - failed_addresses: &[], - other_established: 2, - })); - - // nothing changed - // double penalties for the first three of each - for peer in peers.iter().take(3).chain(others.iter().take(3)) { - assert_eq!( - gs.as_peer_score_mut().score_report(peer).score, - (9.0 + 4.0) * -2.0 - ); - } - - // single penalties for the rest - for peer in peers.iter().skip(3) { - assert_eq!(gs.as_peer_score_mut().score_report(peer).score, 9.0 * -2.0); - } - assert_eq!( - gs.as_peer_score_mut().score_report(&others[3]).score, - 4.0 * -2.0 - ); -} - -#[test] -fn test_scoring_p7_grafts_before_backoff() { - let config = ConfigBuilder::default() - .prune_backoff(Duration::from_millis(200)) - .graft_flood_threshold(Duration::from_millis(100)) - .build() - .unwrap(); - let peer_score_params = PeerScoreParams { - behaviour_penalty_weight: -2.0, - behaviour_penalty_decay: 0.9, - ..Default::default() - }; - - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) - .create_network(); - - // remove peers from mesh and send prune to them => this adds a backoff for the peers - for peer in peers.iter().take(2) { - gs.mesh.get_mut(&topics[0]).unwrap().remove(peer); - gs.send_graft_prune( - HashMap::new(), - HashMap::from([(*peer, vec![topics[0].clone()])]), - HashSet::new(), - ); - } - - // wait 50 millisecs - sleep(Duration::from_millis(50)); - - // first peer tries to graft - gs.handle_graft(&peers[0], vec![topics[0].clone()]); - - // double behaviour penalty for first peer (squared) - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - 4.0 * -2.0 - ); - - // wait 100 millisecs - sleep(Duration::from_millis(100)); - - // second peer tries to graft - gs.handle_graft(&peers[1], vec![topics[0].clone()]); - - // single behaviour penalty for second peer - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[1]).score, - 1.0 * -2.0 - ); - - // test decay - gs.as_peer_score_mut().refresh_scores(); - - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[0]).score, - 4.0 * 0.9 * 0.9 * -2.0 - ); - assert_eq!( - gs.as_peer_score_mut().score_report(&peers[1]).score, - 1.0 * 0.9 * 0.9 * -2.0 - ); -} - -#[test] -fn test_opportunistic_grafting() { - let config = ConfigBuilder::default() - .mesh_n_low(3) - .mesh_n(5) - .mesh_n_high(7) - .mesh_outbound_min(0) // deactivate outbound handling - .opportunistic_graft_ticks(2) - .opportunistic_graft_peers(2) - .build() - .unwrap(); - let peer_score_params = PeerScoreParams { - app_specific_weight: 1.0, - ..Default::default() - }; - let thresholds = PeerScoreThresholds { - opportunistic_graft_threshold: 2.0, - ..Default::default() - }; - - let (mut gs, peers, _receivers, topics) = inject_nodes1() - .peer_no(5) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, thresholds))) - .create_network(); - - // fill mesh with 5 peers - for peer in &peers { - gs.handle_graft(peer, topics.clone()); - } - - // add additional 5 peers - let others: Vec<_> = (0..5) - .map(|_| add_peer(&mut gs, &topics, false, false)) - .collect(); - - // currently mesh equals peers - assert_eq!(gs.mesh[&topics[0]], peers.iter().cloned().collect()); - - // give others high scores (but the first two have not high enough scores) - for (i, peer) in peers.iter().enumerate().take(5) { - gs.set_application_score(peer, 0.0 + i as f64); - } - - // set scores for peers in the mesh - for (i, (peer, _receiver)) in others.iter().enumerate().take(5) { - gs.set_application_score(peer, 0.0 + i as f64); - } - - // this gives a median of exactly 2.0 => should not apply opportunistic grafting - gs.heartbeat(); - gs.heartbeat(); - - assert_eq!( - gs.mesh[&topics[0]].len(), - 5, - "should not apply opportunistic grafting" - ); - - // reduce middle score to 1.0 giving a median of 1.0 - gs.set_application_score(&peers[2], 1.0); - - // opportunistic grafting after two heartbeats - - gs.heartbeat(); - assert_eq!( - gs.mesh[&topics[0]].len(), - 5, - "should not apply opportunistic grafting after first tick" - ); - - gs.heartbeat(); - - assert_eq!( - gs.mesh[&topics[0]].len(), - 7, - "opportunistic grafting should have added 2 peers" - ); - - assert!( - gs.mesh[&topics[0]].is_superset(&peers.iter().cloned().collect()), - "old peers are still part of the mesh" - ); - - assert!( - gs.mesh[&topics[0]].is_disjoint(&others.iter().map(|(p, _)| p).cloned().take(2).collect()), - "peers below or equal to median should not be added in opportunistic grafting" - ); -} - -#[test] -fn test_ignore_graft_from_unknown_topic() { - // build gossipsub without subscribing to any topics - let (mut gs, peers, receivers, _) = inject_nodes1() - .peer_no(1) - .topics(vec![]) - .to_subscribe(false) - .create_network(); - - // handle an incoming graft for some topic - gs.handle_graft(&peers[0], vec![Topic::new("test").hash()]); - - // assert that no prune got created - let (control_msgs, _) = count_control_msgs(receivers, |_, a| matches!(a, RpcOut::Prune { .. })); - assert_eq!( - control_msgs, 0, - "we should not prune after graft in unknown topic" - ); -} - -#[test] -fn test_ignore_too_many_iwants_from_same_peer_for_same_message() { - let config = Config::default(); - // build gossipsub with full mesh - let (mut gs, _, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(false) - .create_network(); - - // add another peer not in the mesh - let (peer, receiver) = add_peer(&mut gs, &topics, false, false); - receivers.insert(peer, receiver); - - // receive a message - let mut seq = 0; - let m1 = random_message(&mut seq, &topics); - - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(m1.clone()).unwrap(); - - let id = config.message_id(message1); - - gs.handle_received_message(m1, &PeerId::random()); - - // clear events - let receivers = flush_events(&mut gs, receivers); - - // the first gossip_retransimission many iwants return the valid message, all others are - // ignored. - for _ in 0..(2 * config.gossip_retransimission() + 10) { - gs.handle_iwant(&peer, vec![id.clone()]); - } - - assert_eq!( - receivers.into_values().fold(0, |mut fwds, c| { - let non_priority = c.non_priority.get_ref(); - while !non_priority.is_empty() { - if let Ok(RpcOut::Forward { .. }) = non_priority.try_recv() { - fwds += 1; - } - } - fwds - }), - config.gossip_retransimission() as usize, - "not more then gossip_retransmission many messages get sent back" - ); -} - -#[test] -fn test_ignore_too_many_ihaves() { - let config = ConfigBuilder::default() - .max_ihave_messages(10) - .build() - .unwrap(); - // build gossipsub with full mesh - let (mut gs, _, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config.clone()) - .create_network(); - - // add another peer not in the mesh - let (peer, receiver) = add_peer(&mut gs, &topics, false, false); - receivers.insert(peer, receiver); - - // peer has 20 messages - let mut seq = 0; - let messages: Vec<_> = (0..20).map(|_| random_message(&mut seq, &topics)).collect(); - - // peer sends us one ihave for each message in order - for raw_message in &messages { - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform(raw_message.clone()) - .unwrap(); - - gs.handle_ihave( - &peer, - vec![(topics[0].clone(), vec![config.message_id(message)])], - ); - } - - let first_ten: HashSet<_> = messages - .iter() - .take(10) - .map(|msg| gs.data_transform.inbound_transform(msg.clone()).unwrap()) - .map(|m| config.message_id(&m)) - .collect(); - - // we send iwant only for the first 10 messages - let (control_msgs, receivers) = count_control_msgs(receivers, |p, action| { - p == &peer - && matches!(action, RpcOut::IWant(IWant { message_ids }) if message_ids.len() == 1 && first_ten.contains(&message_ids[0])) - }); - assert_eq!( - control_msgs, 10, - "exactly the first ten ihaves should be processed and one iwant for each created" - ); - - // after a heartbeat everything is forgotten - gs.heartbeat(); - - for raw_message in messages[10..].iter() { - // Transform the inbound message - let message = &gs - .data_transform - .inbound_transform(raw_message.clone()) - .unwrap(); - - gs.handle_ihave( - &peer, - vec![(topics[0].clone(), vec![config.message_id(message)])], - ); - } - - // we sent iwant for all 10 messages - let (control_msgs, _) = count_control_msgs(receivers, |p, action| { - p == &peer - && matches!(action, RpcOut::IWant(IWant { message_ids }) if message_ids.len() == 1) - }); - assert_eq!(control_msgs, 10, "all 20 should get sent"); -} - -#[test] -fn test_ignore_too_many_messages_in_ihave() { - let config = ConfigBuilder::default() - .max_ihave_messages(10) - .max_ihave_length(10) - .build() - .unwrap(); - // build gossipsub with full mesh - let (mut gs, _, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config.clone()) - .create_network(); - - // add another peer not in the mesh - let (peer, receiver) = add_peer(&mut gs, &topics, false, false); - receivers.insert(peer, receiver); - - // peer has 30 messages - let mut seq = 0; - let message_ids: Vec<_> = (0..30) - .map(|_| random_message(&mut seq, &topics)) - .map(|msg| gs.data_transform.inbound_transform(msg).unwrap()) - .map(|msg| config.message_id(&msg)) - .collect(); - - // peer sends us three ihaves - gs.handle_ihave(&peer, vec![(topics[0].clone(), message_ids[0..8].to_vec())]); - gs.handle_ihave( - &peer, - vec![(topics[0].clone(), message_ids[0..12].to_vec())], - ); - gs.handle_ihave( - &peer, - vec![(topics[0].clone(), message_ids[0..20].to_vec())], - ); - - let first_twelve: HashSet<_> = message_ids.iter().take(12).collect(); - - // we send iwant only for the first 10 messages - let mut sum = 0; - let (control_msgs, receivers) = count_control_msgs(receivers, |p, rpc| match rpc { - RpcOut::IWant(IWant { message_ids }) => { - p == &peer && { - assert!(first_twelve.is_superset(&message_ids.iter().collect())); - sum += message_ids.len(); - true - } - } - _ => false, - }); - assert_eq!( - control_msgs, 2, - "the third ihave should get ignored and no iwant sent" - ); - - assert_eq!(sum, 10, "exactly the first ten ihaves should be processed"); - - // after a heartbeat everything is forgotten - gs.heartbeat(); - gs.handle_ihave( - &peer, - vec![(topics[0].clone(), message_ids[20..30].to_vec())], - ); - - // we sent 10 iwant messages ids via a IWANT rpc. - let mut sum = 0; - let (control_msgs, _) = count_control_msgs(receivers, |p, rpc| match rpc { - RpcOut::IWant(IWant { message_ids }) => { - p == &peer && { - sum += message_ids.len(); - true - } - } - _ => false, - }); - assert_eq!(control_msgs, 1); - assert_eq!(sum, 10, "exactly 20 iwants should get sent"); -} - -#[test] -fn test_limit_number_of_message_ids_inside_ihave() { - let config = ConfigBuilder::default() - .max_ihave_messages(10) - .max_ihave_length(100) - .build() - .unwrap(); - // build gossipsub with full mesh - let (mut gs, peers, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_high()) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config) - .create_network(); - - // graft to all peers to really fill the mesh with all the peers - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // add two other peers not in the mesh - let (p1, receiver1) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p1, receiver1); - let (p2, receiver2) = add_peer(&mut gs, &topics, false, false); - receivers.insert(p2, receiver2); - - // receive 200 messages from another peer - let mut seq = 0; - for _ in 0..200 { - gs.handle_received_message(random_message(&mut seq, &topics), &PeerId::random()); - } - - // emit gossip - gs.emit_gossip(); - - // both peers should have gotten 100 random ihave messages, to assert the randomness, we - // assert that both have not gotten the same set of messages, but have an intersection - // (which is the case with very high probability, the probabiltity of failure is < 10^-58). - - let mut ihaves1 = HashSet::new(); - let mut ihaves2 = HashSet::new(); - - let (control_msgs, _) = count_control_msgs(receivers, |p, action| match action { - RpcOut::IHave(IHave { message_ids, .. }) => { - if p == &p1 { - ihaves1 = message_ids.iter().cloned().collect(); - true - } else if p == &p2 { - ihaves2 = message_ids.iter().cloned().collect(); - true - } else { - false - } - } - _ => false, - }); - assert_eq!( - control_msgs, 2, - "should have emitted one ihave to p1 and one to p2" - ); - - assert_eq!( - ihaves1.len(), - 100, - "should have sent 100 message ids in ihave to p1" - ); - assert_eq!( - ihaves2.len(), - 100, - "should have sent 100 message ids in ihave to p2" - ); - assert!( - ihaves1 != ihaves2, - "should have sent different random messages to p1 and p2 \ - (this may fail with a probability < 10^-58" - ); - assert!( - ihaves1.intersection(&ihaves2).count() > 0, - "should have sent random messages with some common messages to p1 and p2 \ - (this may fail with a probability < 10^-58" - ); -} - -#[test] -fn test_iwant_penalties() { - // use tracing_subscriber::EnvFilter; - // let _ = tracing_subscriber::fmt() - // .with_env_filter(EnvFilter::from_default_env()) - // .try_init(); - let config = ConfigBuilder::default() - .iwant_followup_time(Duration::from_secs(4)) - .build() - .unwrap(); - let peer_score_params = PeerScoreParams { - behaviour_penalty_weight: -1.0, - ..Default::default() - }; - - // fill the mesh - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(2) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config.clone()) - .explicit(0) - .outbound(0) - .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) - .create_network(); - - // graft to all peers to really fill the mesh with all the peers - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - // add 100 more peers - let other_peers: Vec<_> = (0..100) - .map(|_| add_peer(&mut gs, &topics, false, false)) - .collect(); - - // each peer sends us an ihave containing each two message ids - let mut first_messages = Vec::new(); - let mut second_messages = Vec::new(); - let mut seq = 0; - for (peer, _receiver) in &other_peers { - let msg1 = random_message(&mut seq, &topics); - let msg2 = random_message(&mut seq, &topics); - - // Decompress the raw message and calculate the message id. - // Transform the inbound message - let message1 = &gs.data_transform.inbound_transform(msg1.clone()).unwrap(); - - // Transform the inbound message - let message2 = &gs.data_transform.inbound_transform(msg2.clone()).unwrap(); - - first_messages.push(msg1.clone()); - second_messages.push(msg2.clone()); - gs.handle_ihave( - peer, - vec![( - topics[0].clone(), - vec![config.message_id(message1), config.message_id(message2)], - )], - ); - } - - // the peers send us all the first message ids in time - for (index, (peer, _receiver)) in other_peers.iter().enumerate() { - gs.handle_received_message(first_messages[index].clone(), peer); - } - - // now we do a heartbeat no penalization should have been applied yet - gs.heartbeat(); - - for (peer, _receiver) in &other_peers { - assert_eq!(gs.as_peer_score_mut().score_report(peer).score, 0.0); - } - - // receive the first twenty of the other peers then send their response - for (index, (peer, _receiver)) in other_peers.iter().enumerate().take(20) { - gs.handle_received_message(second_messages[index].clone(), peer); - } - - // sleep for the promise duration - sleep(Duration::from_secs(4)); - - // now we do a heartbeat to apply penalization - gs.heartbeat(); - - // now we get the second messages from the last 80 peers. - for (index, (peer, _receiver)) in other_peers.iter().enumerate() { - if index > 19 { - gs.handle_received_message(second_messages[index].clone(), peer); - } - } - - // no further penalizations should get applied - gs.heartbeat(); - - // Only the last 80 peers should be penalized for not responding in time - let mut not_penalized = 0; - let mut single_penalized = 0; - let mut double_penalized = 0; - - for (i, (peer, _receiver)) in other_peers.iter().enumerate() { - let score = gs.as_peer_score_mut().score_report(peer).score; - if score == 0.0 { - not_penalized += 1; - } else if score == -1.0 { - assert!(i > 9); - single_penalized += 1; - } else if score == -4.0 { - assert!(i > 9); - double_penalized += 1 - } else { - println!("{peer}"); - println!("{score}"); - panic!("Invalid score of peer"); - } - } - - assert_eq!(not_penalized, 20); - assert_eq!(single_penalized, 80); - assert_eq!(double_penalized, 0); -} - -#[test] -fn test_publish_to_floodsub_peers_without_flood_publish() { - let config = ConfigBuilder::default() - .flood_publish(false) - .build() - .unwrap(); - let (mut gs, _, mut receivers, topics) = inject_nodes1() - .peer_no(config.mesh_n_low() - 1) - .topics(vec!["test".into()]) - .to_subscribe(false) - .gs_config(config) - .create_network(); - - // add two floodsub peer, one explicit, one implicit - let (p1, receiver1) = add_peer_with_addr_and_kind( - &mut gs, - &topics, - false, - false, - Multiaddr::empty(), - Some(PeerKind::Floodsub), - ); - receivers.insert(p1, receiver1); - - let (p2, receiver2) = - add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); - receivers.insert(p2, receiver2); - - // p1 and p2 are not in the mesh - assert!(!gs.mesh[&topics[0]].contains(&p1) && !gs.mesh[&topics[0]].contains(&p2)); - - // publish a message - let publish_data = vec![0; 42]; - gs.publish(Topic::new("test"), publish_data).unwrap(); - - // Collect publish messages to floodsub peers - let publishes = receivers - .into_iter() - .fold(0, |mut collected_publish, (peer_id, c)| { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if matches!(priority.try_recv(), - Ok(RpcOut::Publish{..}) if peer_id == p1 || peer_id == p2) - { - collected_publish += 1; - } - } - collected_publish - }); - - assert_eq!( - publishes, 2, - "Should send a publish message to all floodsub peers" - ); -} - -#[test] -fn test_do_not_use_floodsub_in_fanout() { - let config = ConfigBuilder::default() - .flood_publish(false) - .build() - .unwrap(); - let (mut gs, _, mut receivers, _) = inject_nodes1() - .peer_no(config.mesh_n_low() - 1) - .topics(Vec::new()) - .to_subscribe(false) - .gs_config(config) - .create_network(); - - let topic = Topic::new("test"); - let topics = vec![topic.hash()]; - - // add two floodsub peer, one explicit, one implicit - let (p1, receiver1) = add_peer_with_addr_and_kind( - &mut gs, - &topics, - false, - false, - Multiaddr::empty(), - Some(PeerKind::Floodsub), - ); - - receivers.insert(p1, receiver1); - let (p2, receiver2) = - add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); - - receivers.insert(p2, receiver2); - // publish a message - let publish_data = vec![0; 42]; - gs.publish(Topic::new("test"), publish_data).unwrap(); - - // Collect publish messages to floodsub peers - let publishes = receivers - .into_iter() - .fold(0, |mut collected_publish, (peer_id, c)| { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if matches!(priority.try_recv(), - Ok(RpcOut::Publish{..}) if peer_id == p1 || peer_id == p2) - { - collected_publish += 1; - } - } - collected_publish - }); - - assert_eq!( - publishes, 2, - "Should send a publish message to all floodsub peers" - ); - - assert!( - !gs.fanout[&topics[0]].contains(&p1) && !gs.fanout[&topics[0]].contains(&p2), - "Floodsub peers are not allowed in fanout" - ); -} - -#[test] -fn test_dont_add_floodsub_peers_to_mesh_on_join() { - let (mut gs, _, _, _) = inject_nodes1() - .peer_no(0) - .topics(Vec::new()) - .to_subscribe(false) - .create_network(); - - let topic = Topic::new("test"); - let topics = vec![topic.hash()]; - - // add two floodsub peer, one explicit, one implicit - let _p1 = add_peer_with_addr_and_kind( - &mut gs, - &topics, - false, - false, - Multiaddr::empty(), - Some(PeerKind::Floodsub), - ); - let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); - - gs.join(&topics[0]); - - assert!( - gs.mesh[&topics[0]].is_empty(), - "Floodsub peers should not get added to mesh" - ); -} - -#[test] -fn test_dont_send_px_to_old_gossipsub_peers() { - let (mut gs, _, receivers, topics) = inject_nodes1() - .peer_no(0) - .topics(vec!["test".into()]) - .to_subscribe(false) - .create_network(); - - // add an old gossipsub peer - let (p1, _receiver1) = add_peer_with_addr_and_kind( - &mut gs, - &topics, - false, - false, - Multiaddr::empty(), - Some(PeerKind::Gossipsub), - ); - - // prune the peer - gs.send_graft_prune( - HashMap::new(), - vec![(p1, topics.clone())].into_iter().collect(), - HashSet::new(), - ); - - // check that prune does not contain px - let (control_msgs, _) = count_control_msgs(receivers, |_, m| match m { - RpcOut::Prune(Prune { peers: px, .. }) => !px.is_empty(), - _ => false, - }); - assert_eq!(control_msgs, 0, "Should not send px to floodsub peers"); -} - -#[test] -fn test_dont_send_floodsub_peers_in_px() { - // build mesh with one peer - let (mut gs, peers, receivers, topics) = inject_nodes1() - .peer_no(1) - .topics(vec!["test".into()]) - .to_subscribe(true) - .create_network(); - - // add two floodsub peers - let _p1 = add_peer_with_addr_and_kind( - &mut gs, - &topics, - false, - false, - Multiaddr::empty(), - Some(PeerKind::Floodsub), - ); - let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); - - // prune only mesh node - gs.send_graft_prune( - HashMap::new(), - vec![(peers[0], topics.clone())].into_iter().collect(), - HashSet::new(), - ); - - // check that px in prune message is empty - let (control_msgs, _) = count_control_msgs(receivers, |_, m| match m { - RpcOut::Prune(Prune { peers: px, .. }) => !px.is_empty(), - _ => false, - }); - assert_eq!(control_msgs, 0, "Should not include floodsub peers in px"); -} - -#[test] -fn test_dont_add_floodsub_peers_to_mesh_in_heartbeat() { - let (mut gs, _, _, topics) = inject_nodes1() - .peer_no(0) - .topics(vec!["test".into()]) - .to_subscribe(false) - .create_network(); - - // add two floodsub peer, one explicit, one implicit - let _p1 = add_peer_with_addr_and_kind( - &mut gs, - &topics, - true, - false, - Multiaddr::empty(), - Some(PeerKind::Floodsub), - ); - let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, true, false, Multiaddr::empty(), None); - - gs.heartbeat(); - - assert!( - gs.mesh[&topics[0]].is_empty(), - "Floodsub peers should not get added to mesh" - ); -} - -// Some very basic test of public api methods. -#[test] -fn test_public_api() { - let (gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(4) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .create_network(); - let peers = peers.into_iter().collect::>(); - - assert_eq!( - gs.topics().cloned().collect::>(), - topic_hashes, - "Expected topics to match registered topic." - ); - - assert_eq!( - gs.mesh_peers(&TopicHash::from_raw("topic1")) - .cloned() - .collect::>(), - peers, - "Expected peers for a registered topic to contain all peers." - ); - - assert_eq!( - gs.all_mesh_peers().cloned().collect::>(), - peers, - "Expected all_peers to contain all peers." - ); -} - -#[test] -fn test_subscribe_to_invalid_topic() { - let t1 = Topic::new("t1"); - let t2 = Topic::new("t2"); - let (mut gs, _, _, _) = inject_nodes::() - .subscription_filter(WhitelistSubscriptionFilter( - vec![t1.hash()].into_iter().collect(), - )) - .to_subscribe(false) - .create_network(); - - assert!(gs.subscribe(&t1).is_ok()); - assert!(gs.subscribe(&t2).is_err()); -} - -#[test] -fn test_subscribe_and_graft_with_negative_score() { - // simulate a communication between two gossipsub instances - let (mut gs1, _, _, topic_hashes) = inject_nodes1() - .topics(vec!["test".into()]) - .scoring(Some(( - PeerScoreParams::default(), - PeerScoreThresholds::default(), - ))) - .create_network(); - - let (mut gs2, _, receivers, _) = inject_nodes1().create_network(); - - let connection_id = ConnectionId::new_unchecked(0); - - let topic = Topic::new("test"); - - let (p2, _receiver1) = add_peer(&mut gs1, &Vec::new(), true, false); - let (p1, _receiver2) = add_peer(&mut gs2, &topic_hashes, false, false); - - // add penalty to peer p2 - gs1.as_peer_score_mut().add_penalty(&p2, 1); - - let original_score = gs1.as_peer_score_mut().score_report(&p2).score; - - // subscribe to topic in gs2 - gs2.subscribe(&topic).unwrap(); - - let forward_messages_to_p1 = |gs1: &mut Behaviour<_, _>, - p1: PeerId, - p2: PeerId, - connection_id: ConnectionId, - receivers: HashMap| - -> HashMap { - let new_receivers = HashMap::new(); - for (peer_id, receiver) in receivers.into_iter() { - let non_priority = receiver.non_priority.get_ref(); - match non_priority.try_recv() { - Ok(rpc) if peer_id == p1 => { - gs1.on_connection_handler_event( - p2, - connection_id, - HandlerEvent::Message { - rpc: proto_to_message(&rpc.into_protobuf()), - invalid_messages: vec![], - }, - ); - } - _ => {} - } - } - new_receivers - }; - - // forward the subscribe message - let receivers = forward_messages_to_p1(&mut gs1, p1, p2, connection_id, receivers); - - // heartbeats on both - gs1.heartbeat(); - gs2.heartbeat(); - - // forward messages again - forward_messages_to_p1(&mut gs1, p1, p2, connection_id, receivers); - - // nobody got penalized - assert!(gs1.as_peer_score_mut().score_report(&p2).score >= original_score); -} - -#[test] -/// Test nodes that send grafts without subscriptions. -fn test_graft_without_subscribe() { - // The node should: - // - Create an empty vector in mesh[topic] - // - Send subscription request to all peers - // - run JOIN(topic) - - let topic = String::from("test_subscribe"); - let subscribe_topic = vec![topic.clone()]; - let subscribe_topic_hash = vec![Topic::new(topic.clone()).hash()]; - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(1) - .topics(subscribe_topic) - .to_subscribe(false) - .create_network(); - - assert!( - gs.mesh.contains_key(&topic_hashes[0]), - "Subscribe should add a new entry to the mesh[topic] hashmap" - ); - - // The node sends a graft for the subscribe topic. - gs.handle_graft(&peers[0], subscribe_topic_hash); - - // The node disconnects - disconnect_peer(&mut gs, &peers[0]); - - // We unsubscribe from the topic. - let _ = gs.unsubscribe(&Topic::new(topic)); -} - -/// Test that a node sends IDONTWANT messages to the mesh peers -/// that run Gossipsub v1.2. -#[test] -fn sends_idontwant() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(5) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_2) - .create_network(); - - let local_id = PeerId::random(); - - let message = RawMessage { - source: Some(peers[1]), - data: vec![12u8; 1024], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(message.clone(), &local_id); - assert_eq!( - receivers - .into_iter() - .fold(0, |mut idontwants, (peer_id, c)| { - let non_priority = c.non_priority.get_ref(); - while !non_priority.is_empty() { - if let Ok(RpcOut::IDontWant(_)) = non_priority.try_recv() { - assert_ne!(peer_id, peers[1]); - idontwants += 1; - } - } - idontwants - }), - 3, - "IDONTWANT was not sent" - ); -} - -#[test] -fn doesnt_sends_idontwant_for_lower_message_size() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(5) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_2) - .create_network(); - - let local_id = PeerId::random(); - - let message = RawMessage { - source: Some(peers[1]), - data: vec![12], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - - gs.handle_received_message(message.clone(), &local_id); - assert_eq!( - receivers - .into_iter() - .fold(0, |mut idontwants, (peer_id, c)| { - let non_priority = c.non_priority.get_ref(); - while !non_priority.is_empty() { - if let Ok(RpcOut::IDontWant(_)) = non_priority.try_recv() { - assert_ne!(peer_id, peers[1]); - idontwants += 1; - } - } - idontwants - }), - 0, - "IDONTWANT was sent" - ); -} - -/// Test that a node doesn't send IDONTWANT messages to the mesh peers -/// that don't run Gossipsub v1.2. -#[test] -fn doesnt_send_idontwant() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(5) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_1) - .create_network(); - - let local_id = PeerId::random(); - - let message = RawMessage { - source: Some(peers[1]), - data: vec![12], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - gs.handle_received_message(message.clone(), &local_id); - assert_eq!( - receivers - .into_iter() - .fold(0, |mut idontwants, (peer_id, c)| { - let non_priority = c.non_priority.get_ref(); - while !non_priority.is_empty() { - if matches!(non_priority.try_recv(), Ok(RpcOut::IDontWant(_)) if peer_id != peers[1]) { - idontwants += 1; - } - } - idontwants - }), - 0, - "IDONTWANT were sent" - ); -} - -/// Test that a node doesn't forward a messages to the mesh peers -/// that sent IDONTWANT. -#[test] -fn doesnt_forward_idontwant() { - let (mut gs, peers, receivers, topic_hashes) = inject_nodes1() - .peer_no(4) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_2) - .create_network(); - - let local_id = PeerId::random(); - - let raw_message = RawMessage { - source: Some(peers[1]), - data: vec![12], - sequence_number: Some(0), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: true, - }; - let message = gs - .data_transform - .inbound_transform(raw_message.clone()) - .unwrap(); - let message_id = gs.config.message_id(&message); - let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); - peer.dont_send.insert(message_id, Instant::now()); - - gs.handle_received_message(raw_message.clone(), &local_id); - assert_eq!( - receivers.into_iter().fold(0, |mut fwds, (peer_id, c)| { - let non_priority = c.non_priority.get_ref(); - while !non_priority.is_empty() { - if let Ok(RpcOut::Forward { .. }) = non_priority.try_recv() { - assert_ne!(peer_id, peers[2]); - fwds += 1; - } - } - fwds - }), - 2, - "IDONTWANT was not sent" - ); -} - -/// Test that a node parses an -/// IDONTWANT message to the respective peer. -#[test] -fn parses_idontwant() { - let (mut gs, peers, _receivers, _topic_hashes) = inject_nodes1() - .peer_no(2) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_2) - .create_network(); - - let message_id = MessageId::new(&[0, 1, 2, 3]); - let rpc = Rpc { - messages: vec![], - subscriptions: vec![], - control_msgs: vec![ControlAction::IDontWant(IDontWant { - message_ids: vec![message_id.clone()], - })], - }; - gs.on_connection_handler_event( - peers[1], - ConnectionId::new_unchecked(0), - HandlerEvent::Message { - rpc, - invalid_messages: vec![], - }, - ); - let peer = gs.connected_peers.get_mut(&peers[1]).unwrap(); - assert!(peer.dont_send.get(&message_id).is_some()); -} - -/// Test that a node clears stale IDONTWANT messages. -#[test] -fn clear_stale_idontwant() { - let (mut gs, peers, _receivers, _topic_hashes) = inject_nodes1() - .peer_no(4) - .topics(vec![String::from("topic1")]) - .to_subscribe(true) - .gs_config(Config::default()) - .explicit(1) - .peer_kind(PeerKind::Gossipsubv1_2) - .create_network(); - - let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); - peer.dont_send - .insert(MessageId::new(&[1, 2, 3, 4]), Instant::now()); - std::thread::sleep(Duration::from_secs(3)); - gs.heartbeat(); - let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); - assert!(peer.dont_send.is_empty()); -} - -#[test] -fn test_all_queues_full() { - let gs_config = ConfigBuilder::default() - .validation_mode(ValidationMode::Permissive) - .build() - .unwrap(); - - let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); - - let topic_hash = Topic::new("Test").hash(); - let mut peers = vec![]; - let mut topics = BTreeSet::new(); - topics.insert(topic_hash.clone()); - - let peer_id = PeerId::random(); - peers.push(peer_id); - gs.connected_peers.insert( - peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(2), - dont_send: LinkedHashMap::new(), - }, - ); - - let publish_data = vec![0; 42]; - gs.publish(topic_hash.clone(), publish_data.clone()) - .unwrap(); - let publish_data = vec![2; 59]; - let err = gs.publish(topic_hash, publish_data).unwrap_err(); - assert!(matches!(err, PublishError::AllQueuesFull(f) if f == 1)); -} - -#[test] -fn test_slow_peer_returns_failed_publish() { - let gs_config = ConfigBuilder::default() - .validation_mode(ValidationMode::Permissive) - .build() - .unwrap(); - - let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); - - let topic_hash = Topic::new("Test").hash(); - let mut peers = vec![]; - let mut topics = BTreeSet::new(); - topics.insert(topic_hash.clone()); - - let slow_peer_id = PeerId::random(); - peers.push(slow_peer_id); - gs.connected_peers.insert( - slow_peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(2), - dont_send: LinkedHashMap::new(), - }, - ); - let peer_id = PeerId::random(); - peers.push(peer_id); - gs.connected_peers.insert( - peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(gs.config.connection_handler_queue_len()), - dont_send: LinkedHashMap::new(), - }, - ); - - let publish_data = vec![0; 42]; - gs.publish(topic_hash.clone(), publish_data.clone()) - .unwrap(); - let publish_data = vec![2; 59]; - gs.publish(topic_hash.clone(), publish_data).unwrap(); - gs.heartbeat(); - - gs.heartbeat(); - - let slow_peer_failed_messages = match gs.events.pop_front().unwrap() { - ToSwarm::GenerateEvent(Event::SlowPeer { - peer_id, - failed_messages, - }) if peer_id == slow_peer_id => failed_messages, - _ => panic!("invalid event"), - }; - - let failed_messages = FailedMessages { - publish: 1, - forward: 0, - priority: 1, - non_priority: 0, - timeout: 0, - }; - - assert_eq!(slow_peer_failed_messages.priority, failed_messages.priority); - assert_eq!( - slow_peer_failed_messages.non_priority, - failed_messages.non_priority - ); - assert_eq!(slow_peer_failed_messages.publish, failed_messages.publish); - assert_eq!(slow_peer_failed_messages.forward, failed_messages.forward); -} - -#[test] -fn test_slow_peer_returns_failed_ihave_handling() { - let gs_config = ConfigBuilder::default() - .validation_mode(ValidationMode::Permissive) - .build() - .unwrap(); - - let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); - - let topic_hash = Topic::new("Test").hash(); - let mut peers = vec![]; - let mut topics = BTreeSet::new(); - topics.insert(topic_hash.clone()); - - let slow_peer_id = PeerId::random(); - gs.connected_peers.insert( - slow_peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(2), - dont_send: LinkedHashMap::new(), - }, - ); - peers.push(slow_peer_id); - let mesh = gs.mesh.entry(topic_hash.clone()).or_default(); - mesh.insert(slow_peer_id); - - let peer_id = PeerId::random(); - peers.push(peer_id); - gs.connected_peers.insert( - peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(gs.config.connection_handler_queue_len()), - dont_send: LinkedHashMap::new(), - }, - ); - - // First message. - let publish_data = vec![1; 59]; - let transformed = gs - .data_transform - .outbound_transform(&topic_hash, publish_data.clone()) - .unwrap(); - let raw_message = gs - .build_raw_message(topic_hash.clone(), transformed) - .unwrap(); - let msg_id = gs.config.message_id(&Message { - source: raw_message.source, - data: publish_data, - sequence_number: raw_message.sequence_number, - topic: raw_message.topic.clone(), - }); - - gs.handle_ihave( - &slow_peer_id, - vec![(topic_hash.clone(), vec![msg_id.clone()])], - ); - - // Second message. - let publish_data = vec![2; 59]; - let transformed = gs - .data_transform - .outbound_transform(&topic_hash, publish_data.clone()) - .unwrap(); - let raw_message = gs - .build_raw_message(topic_hash.clone(), transformed) - .unwrap(); - let msg_id = gs.config.message_id(&Message { - source: raw_message.source, - data: publish_data, - sequence_number: raw_message.sequence_number, - topic: raw_message.topic.clone(), - }); - gs.handle_ihave(&slow_peer_id, vec![(topic_hash, vec![msg_id.clone()])]); - - gs.heartbeat(); - - let slow_peer_failed_messages = gs - .events - .into_iter() - .find_map(|e| match e { - ToSwarm::GenerateEvent(Event::SlowPeer { - peer_id, - failed_messages, - }) if peer_id == slow_peer_id => Some(failed_messages), - _ => None, - }) - .unwrap(); - - let failed_messages = FailedMessages { - publish: 0, - forward: 0, - priority: 0, - non_priority: 1, - timeout: 0, - }; - - assert_eq!(slow_peer_failed_messages.priority, failed_messages.priority); - assert_eq!( - slow_peer_failed_messages.non_priority, - failed_messages.non_priority - ); - assert_eq!(slow_peer_failed_messages.publish, failed_messages.publish); - assert_eq!(slow_peer_failed_messages.forward, failed_messages.forward); -} - -#[test] -fn test_slow_peer_returns_failed_iwant_handling() { - let gs_config = ConfigBuilder::default() - .validation_mode(ValidationMode::Permissive) - .build() - .unwrap(); - - let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); - - let topic_hash = Topic::new("Test").hash(); - let mut peers = vec![]; - let mut topics = BTreeSet::new(); - topics.insert(topic_hash.clone()); - - let slow_peer_id = PeerId::random(); - peers.push(slow_peer_id); - gs.connected_peers.insert( - slow_peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(2), - dont_send: LinkedHashMap::new(), - }, - ); - peers.push(slow_peer_id); - let mesh = gs.mesh.entry(topic_hash.clone()).or_default(); - mesh.insert(slow_peer_id); - - let peer_id = PeerId::random(); - peers.push(peer_id); - gs.connected_peers.insert( - peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(gs.config.connection_handler_queue_len()), - dont_send: LinkedHashMap::new(), - }, - ); - - let publish_data = vec![1; 59]; - let transformed = gs - .data_transform - .outbound_transform(&topic_hash, publish_data.clone()) - .unwrap(); - let raw_message = gs - .build_raw_message(topic_hash.clone(), transformed) - .unwrap(); - let msg_id = gs.config.message_id(&Message { - source: raw_message.source, - data: publish_data, - sequence_number: raw_message.sequence_number, - topic: raw_message.topic.clone(), - }); - - gs.mcache.put(&msg_id, raw_message); - gs.handle_iwant(&slow_peer_id, vec![msg_id.clone(), msg_id]); - - gs.heartbeat(); - - let slow_peer_failed_messages = gs - .events - .into_iter() - .find_map(|e| match e { - ToSwarm::GenerateEvent(Event::SlowPeer { - peer_id, - failed_messages, - }) if peer_id == slow_peer_id => Some(failed_messages), - _ => None, - }) - .unwrap(); - - let failed_messages = FailedMessages { - publish: 0, - forward: 1, - priority: 0, - non_priority: 1, - timeout: 0, - }; - - assert_eq!(slow_peer_failed_messages.priority, failed_messages.priority); - assert_eq!( - slow_peer_failed_messages.non_priority, - failed_messages.non_priority - ); - assert_eq!(slow_peer_failed_messages.publish, failed_messages.publish); - assert_eq!(slow_peer_failed_messages.forward, failed_messages.forward); -} - -#[test] -fn test_slow_peer_returns_failed_forward() { - let gs_config = ConfigBuilder::default() - .validation_mode(ValidationMode::Permissive) - .build() - .unwrap(); - - let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); - - let topic_hash = Topic::new("Test").hash(); - let mut peers = vec![]; - let mut topics = BTreeSet::new(); - topics.insert(topic_hash.clone()); - - let slow_peer_id = PeerId::random(); - peers.push(slow_peer_id); - gs.connected_peers.insert( - slow_peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(2), - dont_send: LinkedHashMap::new(), - }, - ); - peers.push(slow_peer_id); - let mesh = gs.mesh.entry(topic_hash.clone()).or_default(); - mesh.insert(slow_peer_id); - - let peer_id = PeerId::random(); - peers.push(peer_id); - gs.connected_peers.insert( - peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(gs.config.connection_handler_queue_len()), - dont_send: LinkedHashMap::new(), - }, - ); - - let publish_data = vec![1; 59]; - let transformed = gs - .data_transform - .outbound_transform(&topic_hash, publish_data.clone()) - .unwrap(); - let raw_message = gs - .build_raw_message(topic_hash.clone(), transformed) - .unwrap(); - let msg_id = gs.config.message_id(&Message { - source: raw_message.source, - data: publish_data, - sequence_number: raw_message.sequence_number, - topic: raw_message.topic.clone(), - }); - - gs.forward_msg(&msg_id, raw_message.clone(), None, HashSet::new()); - gs.forward_msg(&msg_id, raw_message, None, HashSet::new()); - - gs.heartbeat(); - - let slow_peer_failed_messages = gs - .events - .into_iter() - .find_map(|e| match e { - ToSwarm::GenerateEvent(Event::SlowPeer { - peer_id, - failed_messages, - }) if peer_id == slow_peer_id => Some(failed_messages), - _ => None, - }) - .unwrap(); - - let failed_messages = FailedMessages { - publish: 0, - forward: 1, - priority: 0, - non_priority: 1, - timeout: 0, - }; - - assert_eq!(slow_peer_failed_messages.priority, failed_messages.priority); - assert_eq!( - slow_peer_failed_messages.non_priority, - failed_messages.non_priority - ); - assert_eq!(slow_peer_failed_messages.publish, failed_messages.publish); - assert_eq!(slow_peer_failed_messages.forward, failed_messages.forward); -} - -#[test] -fn test_slow_peer_is_downscored_on_publish() { - let gs_config = ConfigBuilder::default() - .validation_mode(ValidationMode::Permissive) - .build() - .unwrap(); - - let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); - let slow_peer_params = PeerScoreParams::default(); - gs.with_peer_score(slow_peer_params.clone(), PeerScoreThresholds::default()) - .unwrap(); - - let topic_hash = Topic::new("Test").hash(); - let mut peers = vec![]; - let mut topics = BTreeSet::new(); - topics.insert(topic_hash.clone()); - - let slow_peer_id = PeerId::random(); - peers.push(slow_peer_id); - let mesh = gs.mesh.entry(topic_hash.clone()).or_default(); - mesh.insert(slow_peer_id); - gs.connected_peers.insert( - slow_peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(2), - dont_send: LinkedHashMap::new(), - }, - ); - gs.as_peer_score_mut().add_peer(slow_peer_id); - let peer_id = PeerId::random(); - peers.push(peer_id); - gs.connected_peers.insert( - peer_id, - PeerDetails { - kind: PeerKind::Gossipsubv1_1, - connections: vec![ConnectionId::new_unchecked(0)], - outbound: false, - topics: topics.clone(), - sender: Sender::new(gs.config.connection_handler_queue_len()), - dont_send: LinkedHashMap::new(), - }, - ); - - let publish_data = vec![0; 42]; - gs.publish(topic_hash.clone(), publish_data.clone()) - .unwrap(); - let publish_data = vec![2; 59]; - gs.publish(topic_hash.clone(), publish_data).unwrap(); - gs.heartbeat(); - let slow_peer_score = gs.as_peer_score_mut().score_report(&slow_peer_id).score; - assert_eq!(slow_peer_score, slow_peer_params.slow_peer_weight); -} - -#[tokio::test] -async fn test_timedout_messages_are_reported() { - let gs_config = ConfigBuilder::default() - .validation_mode(ValidationMode::Permissive) - .build() - .unwrap(); - - let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); - - let sender = Sender::new(2); - let topic_hash = Topic::new("Test").hash(); - let publish_data = vec![2; 59]; - let raw_message = gs.build_raw_message(topic_hash, publish_data).unwrap(); - - sender - .send_message(RpcOut::Publish { - message: raw_message, - timeout: Delay::new(Duration::from_nanos(1)), - }) - .unwrap(); - let mut receiver = sender.new_receiver(); - let stale = future::poll_fn(|cx| receiver.poll_stale(cx)).await.unwrap(); - assert!(matches!(stale, RpcOut::Publish { .. })); -} - -#[test] -fn test_priority_messages_are_always_sent() { - let sender = Sender::new(2); - let topic_hash = Topic::new("Test").hash(); - // Fill the buffer with the first message. - assert!(sender - .send_message(RpcOut::Subscribe(topic_hash.clone())) - .is_ok()); - assert!(sender - .send_message(RpcOut::Subscribe(topic_hash.clone())) - .is_ok()); - assert!(sender.send_message(RpcOut::Unsubscribe(topic_hash)).is_ok()); -} - -/// Test that specific topic configurations are correctly applied -#[test] -fn test_topic_specific_config() { - let topic_hash1 = Topic::new("topic1").hash(); - let topic_hash2 = Topic::new("topic2").hash(); - - let topic_config1 = TopicMeshConfig { - mesh_n: 5, - mesh_n_low: 3, - mesh_n_high: 10, - mesh_outbound_min: 2, - }; - - let topic_config2 = TopicMeshConfig { - mesh_n: 8, - mesh_n_low: 4, - mesh_n_high: 12, - mesh_outbound_min: 3, - }; - - let config = ConfigBuilder::default() - .set_topic_config(topic_hash1.clone(), topic_config1) - .set_topic_config(topic_hash2.clone(), topic_config2) - .build() - .unwrap(); - - assert_eq!(config.mesh_n_for_topic(&topic_hash1), 5); - assert_eq!(config.mesh_n_low_for_topic(&topic_hash1), 3); - assert_eq!(config.mesh_n_high_for_topic(&topic_hash1), 10); - assert_eq!(config.mesh_outbound_min_for_topic(&topic_hash1), 2); - - assert_eq!(config.mesh_n_for_topic(&topic_hash2), 8); - assert_eq!(config.mesh_n_low_for_topic(&topic_hash2), 4); - assert_eq!(config.mesh_n_high_for_topic(&topic_hash2), 12); - assert_eq!(config.mesh_outbound_min_for_topic(&topic_hash2), 3); - - let topic_hash3 = TopicHash::from_raw("topic3"); - - assert_eq!(config.mesh_n_for_topic(&topic_hash3), config.mesh_n()); - assert_eq!( - config.mesh_n_low_for_topic(&topic_hash3), - config.mesh_n_low() - ); - assert_eq!( - config.mesh_n_high_for_topic(&topic_hash3), - config.mesh_n_high() - ); - assert_eq!( - config.mesh_outbound_min_for_topic(&topic_hash3), - config.mesh_outbound_min() - ); -} - -/// Test mesh maintenance with topic-specific configurations -#[test] -fn test_topic_mesh_maintenance_with_specific_config() { - let topic1_hash = TopicHash::from_raw("topic1"); - let topic2_hash = TopicHash::from_raw("topic2"); - - let topic_config1 = TopicMeshConfig { - mesh_n: 4, - mesh_n_low: 2, - mesh_n_high: 6, - mesh_outbound_min: 1, - }; - - let topic_config2 = TopicMeshConfig { - mesh_n: 8, - mesh_n_low: 4, - mesh_n_high: 12, - mesh_outbound_min: 3, - }; - - let config = ConfigBuilder::default() - .set_topic_config(topic1_hash, topic_config1) - .set_topic_config(topic2_hash, topic_config2) - .build() - .unwrap(); - - let (mut gs, _, _, topic_hashes) = inject_nodes1() - .peer_no(15) - .topics(vec!["topic1".into(), "topic2".into()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - assert_eq!( - gs.mesh.get(&topic_hashes[0]).unwrap().len(), - 2, - "topic1 should have mesh_n 2 peers" - ); - assert_eq!( - gs.mesh.get(&topic_hashes[1]).unwrap().len(), - 4, - "topic2 should have mesh_n 4 peers" - ); - - // run a heartbeat - gs.heartbeat(); - - assert_eq!( - gs.mesh.get(&topic_hashes[0]).unwrap().len(), - 2, - "topic1 should maintain mesh_n 2 peers after heartbeat" - ); - assert_eq!( - gs.mesh.get(&topic_hashes[1]).unwrap().len(), - 4, - "topic2 should maintain mesh_n 4 peers after heartbeat" - ); -} - -/// Test mesh addition with topic-specific configuration -#[test] -fn test_mesh_addition_with_topic_config() { - let topic = String::from("topic1"); - let topic_hash = TopicHash::from_raw(topic.clone()); - - let topic_config = TopicMeshConfig { - mesh_n: 6, - mesh_n_low: 3, - mesh_n_high: 9, - mesh_outbound_min: 2, - }; - - let config = ConfigBuilder::default() - .set_topic_config(topic_hash.clone(), topic_config.clone()) - .build() - .unwrap(); - - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(config.mesh_n_for_topic(&topic_hash) + 1) - .topics(vec![topic]) - .to_subscribe(true) - .gs_config(config.clone()) - .create_network(); - - let to_remove_peers = 1; - - for peer in peers.iter().take(to_remove_peers) { - gs.handle_prune( - peer, - topics.iter().map(|h| (h.clone(), vec![], None)).collect(), - ); - } - - assert_eq!( - gs.mesh.get(&topics[0]).unwrap().len(), - config.mesh_n_low_for_topic(&topic_hash) - 1 - ); - - // run a heartbeat - gs.heartbeat(); - - // Peers should be added to reach mesh_n - assert_eq!( - gs.mesh.get(&topics[0]).unwrap().len(), - config.mesh_n_for_topic(&topic_hash) - ); -} - -/// Test mesh subtraction with topic-specific configuration -#[test] -fn test_mesh_subtraction_with_topic_config() { - let topic = String::from("topic1"); - let topic_hash = TopicHash::from_raw(topic.clone()); - - let topic_config = TopicMeshConfig { - mesh_n: 5, - mesh_n_low: 3, - mesh_n_high: 7, - mesh_outbound_min: 2, - }; - - let config = ConfigBuilder::default() - .set_topic_config(topic_hash.clone(), topic_config) - .build() - .unwrap(); - - let peer_no = 12; - - // make all outbound connections so grafting to all will be allowed - let (mut gs, peers, _, topics) = inject_nodes1() - .peer_no(peer_no) - .topics(vec![topic]) - .to_subscribe(true) - .gs_config(config.clone()) - .outbound(peer_no) - .create_network(); - - // graft all peers - for peer in peers { - gs.handle_graft(&peer, topics.clone()); - } - - assert_eq!( - gs.mesh.get(&topics[0]).unwrap().len(), - peer_no, - "Initially all peers should be in the mesh" - ); - - // run a heartbeat - gs.heartbeat(); - - // Peers should be removed to reach mesh_n - assert_eq!( - gs.mesh.get(&topics[0]).unwrap().len(), - 5, - "After heartbeat, mesh should be reduced to mesh_n 5 peers" - ); -} - -/// Test behavior with multiple topics having different configs -#[test] -fn test_multiple_topics_with_different_configs() { - let topic1 = String::from("topic1"); - let topic2 = String::from("topic2"); - let topic3 = String::from("topic3"); - - let topic_hash1 = TopicHash::from_raw(topic1.clone()); - let topic_hash2 = TopicHash::from_raw(topic2.clone()); - let topic_hash3 = TopicHash::from_raw(topic3.clone()); - - let config1 = TopicMeshConfig { - mesh_n: 4, - mesh_n_low: 3, - mesh_n_high: 6, - mesh_outbound_min: 1, - }; - - let config2 = TopicMeshConfig { - mesh_n: 6, - mesh_n_low: 4, - mesh_n_high: 9, - mesh_outbound_min: 2, - }; - - let config3 = TopicMeshConfig { - mesh_n: 9, - mesh_n_low: 6, - mesh_n_high: 13, - mesh_outbound_min: 3, - }; - - let config = ConfigBuilder::default() - .set_topic_config(topic_hash1.clone(), config1) - .set_topic_config(topic_hash2.clone(), config2) - .set_topic_config(topic_hash3.clone(), config3) - .build() - .unwrap(); - - // Create network with many peers and three topics - let (mut gs, _, _, topic_hashes) = inject_nodes1() - .peer_no(35) - .topics(vec![topic1, topic2, topic3]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - // Check that mesh sizes match each topic's config - assert_eq!( - gs.mesh.get(&topic_hashes[0]).unwrap().len(), - 3, - "topic1 should have 3 peers" - ); - assert_eq!( - gs.mesh.get(&topic_hashes[1]).unwrap().len(), - 4, - "topic2 should have 4 peers" - ); - assert_eq!( - gs.mesh.get(&topic_hashes[2]).unwrap().len(), - 6, - "topic3 should have 6 peers" - ); - - // run a heartbeat - gs.heartbeat(); - - // Verify mesh sizes remain correct after maintenance. The mesh parameters are > mesh_n_low and - // < mesh_n_high so the implementation will maintain the mesh at mesh_n_low. - assert_eq!( - gs.mesh.get(&topic_hashes[0]).unwrap().len(), - 3, - "topic1 should maintain 3 peers after heartbeat" - ); - assert_eq!( - gs.mesh.get(&topic_hashes[1]).unwrap().len(), - 4, - "topic2 should maintain 4 peers after heartbeat" - ); - assert_eq!( - gs.mesh.get(&topic_hashes[2]).unwrap().len(), - 6, - "topic3 should maintain 6 peers after heartbeat" - ); - - // Unsubscribe from topic1 - assert!( - gs.unsubscribe(&Topic::new(topic_hashes[0].to_string())), - "Should unsubscribe successfully" - ); - - // verify it's removed from mesh - assert!( - !gs.mesh.contains_key(&topic_hashes[0]), - "topic1 should be removed from mesh after unsubscribe" - ); - - // re-subscribe to topic1 - assert!( - gs.subscribe(&Topic::new(topic_hashes[0].to_string())) - .unwrap(), - "Should subscribe successfully" - ); - - // Verify mesh is recreated with correct size - assert_eq!( - gs.mesh.get(&topic_hashes[0]).unwrap().len(), - 4, - "topic1 should have mesh_n 4 peers after re-subscribe" - ); -} - -/// Test fanout behavior with topic-specific configuration -#[test] -fn test_fanout_with_topic_config() { - let topic = String::from("topic1"); - let topic_hash = TopicHash::from_raw(topic.clone()); - - let topic_config = TopicMeshConfig { - mesh_n: 4, - mesh_n_low: 2, - mesh_n_high: 7, - mesh_outbound_min: 1, - }; - - // turn off flood publish to test fanout behaviour - let config = ConfigBuilder::default() - .flood_publish(false) - .set_topic_config(topic_hash.clone(), topic_config) - .build() - .unwrap(); - - let (mut gs, _, receivers, topic_hashes) = inject_nodes1() - .peer_no(10) // More than mesh_n - .topics(vec![topic.clone()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - assert!( - gs.unsubscribe(&Topic::new(topic.clone())), - "Should unsubscribe successfully" - ); - - let publish_data = vec![0; 42]; - gs.publish(Topic::new(topic), publish_data).unwrap(); - - // Check that fanout size matches the topic-specific mesh_n - assert_eq!( - gs.fanout.get(&topic_hashes[0]).unwrap().len(), - 4, - "Fanout should contain topic-specific mesh_n 4 peers for this topic" - ); - - // Collect publish messages - let publishes = receivers - .into_values() - .fold(vec![], |mut collected_publish, c| { - let priority = c.priority.get_ref(); - while !priority.is_empty() { - if let Ok(RpcOut::Publish { message, .. }) = priority.try_recv() { - collected_publish.push(message); - } - } - collected_publish - }); - - // Verify sent to topic-specific mesh_n number of peers - assert_eq!( - publishes.len(), - 4, - "Should send a publish message to topic-specific mesh_n 4 fanout peers" - ); -} - -#[test] -fn test_publish_message_with_default_transmit_size_config() { - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - - let config = ConfigBuilder::default() - .set_topic_max_transmit_size(topic_hash.clone(), Config::default_max_transmit_size()) - .validation_mode(ValidationMode::Strict) - .build() - .unwrap(); - - let (mut gs, _, _, _) = inject_nodes1() - .peer_no(10) - .topics(vec!["test".to_string()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - let data = vec![0; 1024]; - - let result = gs.publish(topic.clone(), data); - assert!( - result.is_ok(), - "Expected successful publish within size limit" - ); - let msg_id = result.unwrap(); - assert!( - gs.mcache.get(&msg_id).is_some(), - "Message should be in cache" - ); -} - -#[test] -fn test_publish_large_message_with_default_transmit_size_config() { - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - - let config = ConfigBuilder::default() - .set_topic_max_transmit_size(topic_hash.clone(), Config::default_max_transmit_size()) - .validation_mode(ValidationMode::Strict) - .build() - .unwrap(); - - let (mut gs, _, _, _) = inject_nodes1() - .peer_no(10) - .topics(vec!["test".to_string()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - let data = vec![0; Config::default_max_transmit_size() + 1]; - - let result = gs.publish(topic.clone(), data); - assert!( - matches!(result, Err(PublishError::MessageTooLarge)), - "Expected MessageTooLarge error for oversized message" - ); -} - -#[test] -fn test_publish_message_with_specific_transmit_size_config() { - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - - let max_topic_transmit_size = 2000; - let config = ConfigBuilder::default() - .set_topic_max_transmit_size(topic_hash.clone(), max_topic_transmit_size) - .validation_mode(ValidationMode::Strict) - .build() - .unwrap(); - - let (mut gs, _, _, _) = inject_nodes1() - .peer_no(10) - .topics(vec!["test".to_string()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - let data = vec![0; 1024]; - - let result = gs.publish(topic.clone(), data); - assert!( - result.is_ok(), - "Expected successful publish within topic-specific size limit" - ); - let msg_id = result.unwrap(); - assert!( - gs.mcache.get(&msg_id).is_some(), - "Message should be in cache" - ); -} - -#[test] -fn test_publish_large_message_with_specific_transmit_size_config() { - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - - let max_topic_transmit_size = 2048; - let config = ConfigBuilder::default() - .set_topic_max_transmit_size(topic_hash.clone(), max_topic_transmit_size) - .validation_mode(ValidationMode::Strict) - .build() - .unwrap(); - - let (mut gs, _, _, _) = inject_nodes1() - .peer_no(10) - .topics(vec!["test".to_string()]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - let data = vec![0; 2049]; - - let result = gs.publish(topic.clone(), data); - assert!( - matches!(result, Err(PublishError::MessageTooLarge)), - "Expected MessageTooLarge error for oversized message with topic-specific config" - ); -} - -#[test] -fn test_validation_error_message_size_too_large_topic_specific() { - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let max_size = 2048; - - let config = ConfigBuilder::default() - .set_topic_max_transmit_size(topic_hash.clone(), max_size) - .validation_mode(ValidationMode::None) - .build() - .unwrap(); - - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(1) - .topics(vec![String::from("test")]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - let data = vec![0u8; max_size + 1]; - let raw_message = RawMessage { - source: Some(peers[0]), - data, - sequence_number: Some(1u64), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: false, - }; - - gs.on_connection_handler_event( - peers[0], - ConnectionId::new_unchecked(0), - HandlerEvent::Message { - rpc: Rpc { - messages: vec![raw_message], - subscriptions: vec![], - control_msgs: vec![], - }, - invalid_messages: vec![], - }, - ); - - let event = gs.events.pop_front().expect("Event should be generated"); - match event { - ToSwarm::GenerateEvent(Event::Message { - propagation_source, - message_id: _, - message, - }) => { - assert_eq!(propagation_source, peers[0]); - assert_eq!(message.data.len(), max_size + 1); - } - ToSwarm::NotifyHandler { peer_id, .. } => { - assert_eq!(peer_id, peers[0]); - } - _ => panic!("Unexpected event"), - } - - // Simulate a peer sending a message exceeding the topic-specific max_transmit_size (2048 - // bytes). The codec's max_length is set high to allow encoding/decoding the RPC, while - // max_transmit_sizes enforces the custom topic limit. - let mut max_transmit_size_map = HashMap::new(); - max_transmit_size_map.insert(topic_hash, max_size); - - let mut codec = GossipsubCodec::new( - Config::default_max_transmit_size() * 2, - ValidationMode::None, - max_transmit_size_map, - ); - let mut buf = BytesMut::new(); - let rpc = proto::RPC { - publish: vec![proto::Message { - from: Some(peers[0].to_bytes()), - data: Some(vec![0u8; max_size + 1]), - seqno: Some(1u64.to_be_bytes().to_vec()), - topic: topic_hashes[0].to_string(), - signature: None, - key: None, - }], - subscriptions: vec![], - control: None, - }; - codec.encode(rpc, &mut buf).unwrap(); - - let decoded = codec.decode(&mut buf).unwrap().unwrap(); - match decoded { - HandlerEvent::Message { - rpc, - invalid_messages, - } => { - assert!( - rpc.messages.is_empty(), - "No valid messages should be present" - ); - assert_eq!(invalid_messages.len(), 1, "One message should be invalid"); - let (invalid_msg, error) = &invalid_messages[0]; - assert_eq!(invalid_msg.data.len(), max_size + 1); - assert_eq!(error, &ValidationError::MessageSizeTooLargeForTopic); - } - _ => panic!("Unexpected event"), - } -} - -#[test] -fn test_validation_message_size_within_topic_specific() { - let topic = Topic::new("test"); - let topic_hash = topic.hash(); - let max_size = 2048; - - let config = ConfigBuilder::default() - .set_topic_max_transmit_size(topic_hash.clone(), max_size) - .validation_mode(ValidationMode::None) - .build() - .unwrap(); - - let (mut gs, peers, _, topic_hashes) = inject_nodes1() - .peer_no(1) - .topics(vec![String::from("test")]) - .to_subscribe(true) - .gs_config(config) - .create_network(); - - let data = vec![0u8; max_size - 100]; - let raw_message = RawMessage { - source: Some(peers[0]), - data, - sequence_number: Some(1u64), - topic: topic_hashes[0].clone(), - signature: None, - key: None, - validated: false, - }; - - gs.on_connection_handler_event( - peers[0], - ConnectionId::new_unchecked(0), - HandlerEvent::Message { - rpc: Rpc { - messages: vec![raw_message], - subscriptions: vec![], - control_msgs: vec![], - }, - invalid_messages: vec![], - }, - ); - - let event = gs.events.pop_front().expect("Event should be generated"); - match event { - ToSwarm::GenerateEvent(Event::Message { - propagation_source, - message_id: _, - message, - }) => { - assert_eq!(propagation_source, peers[0]); - assert_eq!(message.data.len(), max_size - 100); - } - ToSwarm::NotifyHandler { peer_id, .. } => { - assert_eq!(peer_id, peers[0]); - } - _ => panic!("Unexpected event"), - } - - // Simulate a peer sending a message within the topic-specific max_transmit_size (2048 bytes). - // The codec's max_length allows encoding/decoding the RPC, and max_transmit_sizes confirms - // the message size is acceptable for the topic. - let mut max_transmit_size_map = HashMap::new(); - max_transmit_size_map.insert(topic_hash, max_size); - - let mut codec = GossipsubCodec::new( - Config::default_max_transmit_size() * 2, - ValidationMode::None, - max_transmit_size_map, - ); - let mut buf = BytesMut::new(); - let rpc = proto::RPC { - publish: vec![proto::Message { - from: Some(peers[0].to_bytes()), - data: Some(vec![0u8; max_size - 100]), - seqno: Some(1u64.to_be_bytes().to_vec()), - topic: topic_hashes[0].to_string(), - signature: None, - key: None, - }], - subscriptions: vec![], - control: None, - }; - codec.encode(rpc, &mut buf).unwrap(); - - let decoded = codec.decode(&mut buf).unwrap().unwrap(); - match decoded { - HandlerEvent::Message { - rpc, - invalid_messages, - } => { - assert_eq!(rpc.messages.len(), 1, "One valid message should be present"); - assert!(invalid_messages.is_empty(), "No messages should be invalid"); - assert_eq!(rpc.messages[0].data.len(), max_size - 100); - } - _ => panic!("Unexpected event"), - } -} diff --git a/protocols/gossipsub/src/behaviour/tests/explicit_peers.rs b/protocols/gossipsub/src/behaviour/tests/explicit_peers.rs new file mode 100644 index 00000000000..a0d9b1b1e1b --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/explicit_peers.rs @@ -0,0 +1,387 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Tests for explicit peer handling. + +use std::collections::BTreeSet; + +use libp2p_identity::PeerId; +use libp2p_swarm::ToSwarm; + +use super::{count_control_msgs, disconnect_peer, flush_events, DefaultBehaviourTestBuilder}; +use crate::{ + config::{Config, ConfigBuilder}, + types::{Prune, RawMessage, RpcOut, Subscription, SubscriptionAction}, + IdentTopic as Topic, +}; + +/// tests that a peer added as explicit peer gets connected to +#[test] +fn test_explicit_peer_gets_connected() { + let (mut gs, _, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(0) + .topics(Vec::new()) + .to_subscribe(true) + .create_network(); + + // create new peer + let peer = PeerId::random(); + + // add peer as explicit peer + gs.add_explicit_peer(&peer); + + let num_events = gs + .events + .iter() + .filter(|e| match e { + ToSwarm::Dial { opts } => opts.get_peer_id() == Some(peer), + _ => false, + }) + .count(); + + assert_eq!( + num_events, 1, + "There was no dial peer event for the explicit peer" + ); +} + +#[test] +fn test_explicit_peer_reconnects() { + let config = ConfigBuilder::default() + .check_explicit_peers_ticks(2) + .build() + .unwrap(); + let (mut gs, others, queues, _) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(Vec::new()) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + let peer = others.first().unwrap(); + + // add peer as explicit peer + gs.add_explicit_peer(peer); + + flush_events(&mut gs, queues); + + // disconnect peer + disconnect_peer(&mut gs, peer); + + gs.heartbeat(); + + // check that no reconnect after first heartbeat since `explicit_peer_ticks == 2` + assert_eq!( + gs.events + .iter() + .filter(|e| match e { + ToSwarm::Dial { opts } => opts.get_peer_id() == Some(*peer), + _ => false, + }) + .count(), + 0, + "There was a dial peer event before explicit_peer_ticks heartbeats" + ); + + gs.heartbeat(); + + // check that there is a reconnect after second heartbeat + assert!( + gs.events + .iter() + .filter(|e| match e { + ToSwarm::Dial { opts } => opts.get_peer_id() == Some(*peer), + _ => false, + }) + .count() + >= 1, + "There was no dial peer event for the explicit peer" + ); +} + +#[test] +fn test_handle_graft_explicit_peer() { + let (mut gs, peers, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec![String::from("topic1"), String::from("topic2")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + let peer = peers.first().unwrap(); + + gs.handle_graft(peer, topic_hashes.clone()); + + // peer got not added to mesh + assert!(gs.mesh[&topic_hashes[0]].is_empty()); + assert!(gs.mesh[&topic_hashes[1]].is_empty()); + + // check prunes + let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| { + peer_id == peer + && match m { + RpcOut::Prune(Prune { topic_hash, .. }) => { + topic_hash == &topic_hashes[0] || topic_hash == &topic_hashes[1] + } + _ => false, + } + }); + assert!( + control_msgs >= 2, + "Not enough prunes sent when grafting from explicit peer" + ); +} + +#[test] +fn explicit_peers_not_added_to_mesh_on_receiving_subscription() { + let (gs, peers, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + // only peer 1 is in the mesh not peer 0 (which is an explicit peer) + assert_eq!( + gs.mesh[&topic_hashes[0]], + vec![peers[1]].into_iter().collect() + ); + + // assert that graft gets created to non-explicit peer + let (control_msgs, queues) = count_control_msgs(queues, |peer_id, m| { + peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) + }); + assert!( + control_msgs >= 1, + "No graft message got created to non-explicit peer" + ); + + // assert that no graft gets created to explicit peer + let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| { + peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) + }); + assert_eq!( + control_msgs, 0, + "A graft message got created to an explicit peer" + ); +} + +#[test] +fn do_not_graft_explicit_peer() { + let (mut gs, others, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec![String::from("topic")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + gs.heartbeat(); + + // mesh stays empty + assert_eq!(gs.mesh[&topic_hashes[0]], BTreeSet::new()); + + // assert that no graft gets created to explicit peer + let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| { + peer_id == &others[0] && matches!(m, RpcOut::Graft { .. }) + }); + assert_eq!( + control_msgs, 0, + "A graft message got created to an explicit peer" + ); +} + +#[test] +fn do_forward_messages_to_explicit_peers() { + let (mut gs, peers, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(vec![String::from("topic1"), String::from("topic2")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + let local_id = PeerId::random(); + + let message = RawMessage { + source: Some(peers[1]), + data: vec![12], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(message.clone(), &local_id); + assert_eq!( + queues.into_iter().fold(0, |mut fwds, (peer_id, mut queue)| { + while !queue.is_empty() { + if matches!(queue.try_pop(), Some(RpcOut::Forward{message: m, ..}) if peer_id == peers[0] && m.data == message.data) { + fwds +=1; + } + } + fwds + }), + 1, + "The message did not get forwarded to the explicit peer" + ); +} + +#[test] +fn explicit_peers_not_added_to_mesh_on_subscribe() { + let (mut gs, peers, queues, _) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(Vec::new()) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + // create new topic, both peers subscribing to it but we do not subscribe to it + let topic = Topic::new(String::from("t")); + let topic_hash = topic.hash(); + for peer in peers.iter().take(2) { + gs.handle_received_subscriptions( + &[Subscription { + action: SubscriptionAction::Subscribe, + topic_hash: topic_hash.clone(), + }], + peer, + ); + } + + // subscribe now to topic + gs.subscribe(&topic).unwrap(); + + // only peer 1 is in the mesh not peer 0 (which is an explicit peer) + assert_eq!(gs.mesh[&topic_hash], vec![peers[1]].into_iter().collect()); + + // assert that graft gets created to non-explicit peer + let (control_msgs, queues) = count_control_msgs(queues, |peer_id, m| { + peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) + }); + assert!( + control_msgs > 0, + "No graft message got created to non-explicit peer" + ); + + // assert that no graft gets created to explicit peer + let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| { + peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) + }); + assert_eq!( + control_msgs, 0, + "A graft message got created to an explicit peer" + ); +} + +#[test] +fn explicit_peers_not_added_to_mesh_from_fanout_on_subscribe() { + let (mut gs, peers, queues, _) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(Vec::new()) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + // create new topic, both peers subscribing to it but we do not subscribe to it + let topic = Topic::new(String::from("t")); + let topic_hash = topic.hash(); + for peer in peers.iter().take(2) { + gs.handle_received_subscriptions( + &[Subscription { + action: SubscriptionAction::Subscribe, + topic_hash: topic_hash.clone(), + }], + peer, + ); + } + + // we send a message for this topic => this will initialize the fanout + gs.publish(topic.clone(), vec![1, 2, 3]).unwrap(); + + // subscribe now to topic + gs.subscribe(&topic).unwrap(); + + // only peer 1 is in the mesh not peer 0 (which is an explicit peer) + assert_eq!(gs.mesh[&topic_hash], vec![peers[1]].into_iter().collect()); + + // assert that graft gets created to non-explicit peer + let (control_msgs, queues) = count_control_msgs(queues, |peer_id, m| { + peer_id == &peers[1] && matches!(m, RpcOut::Graft { .. }) + }); + assert!( + control_msgs >= 1, + "No graft message got created to non-explicit peer" + ); + + // assert that no graft gets created to explicit peer + let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| { + peer_id == &peers[0] && matches!(m, RpcOut::Graft { .. }) + }); + assert_eq!( + control_msgs, 0, + "A graft message got created to an explicit peer" + ); +} + +#[test] +fn no_gossip_gets_sent_to_explicit_peers() { + let (mut gs, peers, mut queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(vec![String::from("topic1"), String::from("topic2")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .create_network(); + + let local_id = PeerId::random(); + + let message = RawMessage { + source: Some(peers[1]), + data: vec![], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + + // forward the message + gs.handle_received_message(message, &local_id); + + // simulate multiple gossip calls (for randomness) + for _ in 0..3 { + gs.emit_gossip(); + } + + // assert that no gossip gets sent to explicit peer + let mut receiver_queue = queues.remove(&peers[0]).unwrap(); + let mut gossips = 0; + while !receiver_queue.is_empty() { + if let Some(RpcOut::IHave(_)) = receiver_queue.try_pop() { + gossips += 1; + } + } + assert_eq!(gossips, 0, "Gossip got emitted to explicit peer"); +} diff --git a/protocols/gossipsub/src/behaviour/tests/floodsub.rs b/protocols/gossipsub/src/behaviour/tests/floodsub.rs new file mode 100644 index 00000000000..3288de9f469 --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/floodsub.rs @@ -0,0 +1,272 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Tests for floodsub compatibility. + +use std::collections::{HashMap, HashSet}; + +use libp2p_core::Multiaddr; + +use super::{add_peer_with_addr_and_kind, count_control_msgs, DefaultBehaviourTestBuilder}; +use crate::{ + config::ConfigBuilder, + types::{PeerKind, Prune, RpcOut}, + IdentTopic as Topic, +}; + +#[test] +fn test_publish_to_floodsub_peers_without_flood_publish() { + let config = ConfigBuilder::default() + .flood_publish(false) + .build() + .unwrap(); + let (mut gs, _, mut queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_low() - 1) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config) + .create_network(); + + // add two floodsub peer, one explicit, one implicit + let (p1, queue1) = add_peer_with_addr_and_kind( + &mut gs, + &topics, + false, + false, + Multiaddr::empty(), + Some(PeerKind::Floodsub), + ); + queues.insert(p1, queue1); + + let (p2, queue2) = + add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); + queues.insert(p2, queue2); + + // p1 and p2 are not in the mesh + assert!(!gs.mesh[&topics[0]].contains(&p1) && !gs.mesh[&topics[0]].contains(&p2)); + + // publish a message + let publish_data = vec![0; 42]; + gs.publish(Topic::new("test"), publish_data).unwrap(); + + // Collect publish messages to floodsub peers + let publishes = queues + .into_iter() + .fold(0, |mut collected_publish, (peer_id, mut queue)| { + while !queue.is_empty() { + if matches!(queue.try_pop(), + Some(RpcOut::Publish{..}) if peer_id == p1 || peer_id == p2) + { + collected_publish += 1; + } + } + collected_publish + }); + + assert_eq!( + publishes, 2, + "Should send a publish message to all floodsub peers" + ); +} + +#[test] +fn test_do_not_use_floodsub_in_fanout() { + let config = ConfigBuilder::default() + .flood_publish(false) + .build() + .unwrap(); + let (mut gs, _, mut queues, _) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_low() - 1) + .topics(Vec::new()) + .to_subscribe(false) + .gs_config(config) + .create_network(); + + let topic = Topic::new("test"); + let topics = vec![topic.hash()]; + + // add two floodsub peer, one explicit, one implicit + let (p1, queue1) = add_peer_with_addr_and_kind( + &mut gs, + &topics, + false, + false, + Multiaddr::empty(), + Some(PeerKind::Floodsub), + ); + + queues.insert(p1, queue1); + let (p2, queue2) = + add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); + + queues.insert(p2, queue2); + // publish a message + let publish_data = vec![0; 42]; + gs.publish(Topic::new("test"), publish_data).unwrap(); + + // Collect publish messages to floodsub peers + let publishes = queues + .into_iter() + .fold(0, |mut collected_publish, (peer_id, mut queue)| { + while !queue.is_empty() { + if matches!(queue.try_pop(), + Some(RpcOut::Publish{..}) if peer_id == p1 || peer_id == p2) + { + collected_publish += 1; + } + } + collected_publish + }); + + assert_eq!( + publishes, 2, + "Should send a publish message to all floodsub peers" + ); + + assert!( + !gs.fanout[&topics[0]].contains(&p1) && !gs.fanout[&topics[0]].contains(&p2), + "Floodsub peers are not allowed in fanout" + ); +} + +#[test] +fn test_dont_add_floodsub_peers_to_mesh_on_join() { + let (mut gs, _, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(0) + .topics(Vec::new()) + .to_subscribe(false) + .create_network(); + + let topic = Topic::new("test"); + let topics = vec![topic.hash()]; + + // add two floodsub peer, one explicit, one implicit + let _p1 = add_peer_with_addr_and_kind( + &mut gs, + &topics, + false, + false, + Multiaddr::empty(), + Some(PeerKind::Floodsub), + ); + let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); + + gs.join(&topics[0]); + + assert!( + gs.mesh[&topics[0]].is_empty(), + "Floodsub peers should not get added to mesh" + ); +} + +#[test] +fn test_dont_send_px_to_old_gossipsub_peers() { + let (mut gs, _, queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(0) + .topics(vec!["test".into()]) + .to_subscribe(false) + .create_network(); + + // add an old gossipsub peer + let (p1, _queue1) = add_peer_with_addr_and_kind( + &mut gs, + &topics, + false, + false, + Multiaddr::empty(), + Some(PeerKind::Gossipsub), + ); + + // prune the peer + gs.send_graft_prune( + HashMap::new(), + vec![(p1, topics.clone())].into_iter().collect(), + HashSet::new(), + ); + + // check that prune does not contain px + let (control_msgs, _) = count_control_msgs(queues, |_, m| match m { + RpcOut::Prune(Prune { peers: px, .. }) => !px.is_empty(), + _ => false, + }); + assert_eq!(control_msgs, 0, "Should not send px to floodsub peers"); +} + +#[test] +fn test_dont_send_floodsub_peers_in_px() { + // build mesh with one peer + let (mut gs, peers, queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + // add two floodsub peers + let _p1 = add_peer_with_addr_and_kind( + &mut gs, + &topics, + false, + false, + Multiaddr::empty(), + Some(PeerKind::Floodsub), + ); + let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, false, false, Multiaddr::empty(), None); + + // prune only mesh node + gs.send_graft_prune( + HashMap::new(), + vec![(peers[0], topics.clone())].into_iter().collect(), + HashSet::new(), + ); + + // check that px in prune message is empty + let (control_msgs, _) = count_control_msgs(queues, |_, m| match m { + RpcOut::Prune(Prune { peers: px, .. }) => !px.is_empty(), + _ => false, + }); + assert_eq!(control_msgs, 0, "Should not include floodsub peers in px"); +} + +#[test] +fn test_dont_add_floodsub_peers_to_mesh_in_heartbeat() { + let (mut gs, _, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(0) + .topics(vec!["test".into()]) + .to_subscribe(false) + .create_network(); + + // add two floodsub peer, one explicit, one implicit + let _p1 = add_peer_with_addr_and_kind( + &mut gs, + &topics, + true, + false, + Multiaddr::empty(), + Some(PeerKind::Floodsub), + ); + let _p2 = add_peer_with_addr_and_kind(&mut gs, &topics, true, false, Multiaddr::empty(), None); + + gs.heartbeat(); + + assert!( + gs.mesh[&topics[0]].is_empty(), + "Floodsub peers should not get added to mesh" + ); +} diff --git a/protocols/gossipsub/src/behaviour/tests/gossip.rs b/protocols/gossipsub/src/behaviour/tests/gossip.rs new file mode 100644 index 00000000000..fb708cc4d98 --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/gossip.rs @@ -0,0 +1,808 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Tests for IHAVE/IWANT gossip message handling. + +use std::{collections::HashSet, thread::sleep, time::Duration}; + +use libp2p_identity::PeerId; +use libp2p_swarm::{ConnectionId, NetworkBehaviour}; + +use super::{ + add_peer, count_control_msgs, flush_events, random_message, DefaultBehaviourTestBuilder, +}; +use crate::{ + config::{Config, ConfigBuilder}, + handler::HandlerEvent, + peer_score::{PeerScoreParams, PeerScoreThresholds}, + topic::TopicHash, + transform::DataTransform, + types::{ControlAction, IDontWant, IHave, IWant, MessageId, RawMessage, RpcIn, RpcOut}, +}; + +/// Tests that the correct message is sent when a peer asks for a message in our cache. +#[test] +fn test_handle_iwant_msg_cached() { + let (mut gs, peers, queues, _) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(Vec::new()) + .to_subscribe(true) + .create_network(); + + let raw_message = RawMessage { + source: Some(peers[11]), + data: vec![1, 2, 3, 4], + sequence_number: Some(1u64), + topic: TopicHash::from_raw("topic"), + signature: None, + key: None, + validated: true, + }; + + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform(raw_message.clone()) + .unwrap(); + + let msg_id = gs.config.message_id(message); + gs.mcache.put(&msg_id, raw_message); + + gs.handle_iwant(&peers[7], vec![msg_id.clone()]); + + // the messages we are sending + let sent_messages = queues + .into_values() + .fold(vec![], |mut collected_messages, mut queue| { + while !queue.is_empty() { + if let Some(RpcOut::Forward { message, .. }) = queue.try_pop() { + collected_messages.push(message) + } + } + collected_messages + }); + + assert!( + sent_messages + .iter() + .map(|msg| gs.data_transform.inbound_transform(msg.clone()).unwrap()) + .any(|msg| gs.config.message_id(&msg) == msg_id), + "Expected the cached message to be sent to an IWANT peer" + ); +} + +/// Tests that messages are sent correctly depending on the shifting of the message cache. +#[test] +fn test_handle_iwant_msg_cached_shifted() { + let (mut gs, peers, mut queues, _) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(Vec::new()) + .to_subscribe(true) + .create_network(); + + // perform 10 memshifts and check that it leaves the cache + for shift in 1..10 { + let raw_message = RawMessage { + source: Some(peers[11]), + data: vec![1, 2, 3, 4], + sequence_number: Some(shift), + topic: TopicHash::from_raw("topic"), + signature: None, + key: None, + validated: true, + }; + + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform(raw_message.clone()) + .unwrap(); + + let msg_id = gs.config.message_id(message); + gs.mcache.put(&msg_id, raw_message); + for _ in 0..shift { + gs.mcache.shift(); + } + + gs.handle_iwant(&peers[7], vec![msg_id.clone()]); + + // is the message is being sent? + let mut message_exists = false; + queues = queues + .into_iter() + .map(|(peer_id, mut queue)| { + while !queue.is_empty() { + if matches!(queue.try_pop(), Some(RpcOut::Forward{message, ..}) if + gs.config.message_id( + &gs.data_transform + .inbound_transform(message.clone()) + .unwrap(), + ) == msg_id) + { + message_exists = true; + } + } + (peer_id, queue) + }) + .collect(); + // default history_length is 5, expect no messages after shift > 5 + if shift < 5 { + assert!( + message_exists, + "Expected the cached message to be sent to an IWANT peer before 5 shifts" + ); + } else { + assert!( + !message_exists, + "Expected the cached message to not be sent to an IWANT peer after 5 shifts" + ); + } + } +} + +/// tests that an event is not created when a peers asks for a message not in our cache +#[test] +fn test_handle_iwant_msg_not_cached() { + let (mut gs, peers, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(Vec::new()) + .to_subscribe(true) + .create_network(); + + let events_before = gs.events.len(); + gs.handle_iwant(&peers[7], vec![MessageId::new(b"unknown id")]); + let events_after = gs.events.len(); + + assert_eq!( + events_before, events_after, + "Expected event count to stay the same" + ); +} + +#[test] +fn test_handle_iwant_msg_but_already_sent_idontwant() { + let (mut gs, peers, queues, _) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(Vec::new()) + .to_subscribe(true) + .create_network(); + + let raw_message = RawMessage { + source: Some(peers[11]), + data: vec![1, 2, 3, 4], + sequence_number: Some(1u64), + topic: TopicHash::from_raw("topic"), + signature: None, + key: None, + validated: true, + }; + + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform(raw_message.clone()) + .unwrap(); + + let msg_id = gs.config.message_id(message); + gs.mcache.put(&msg_id, raw_message); + + // Receive IDONTWANT from Peer 1. + let rpc = RpcIn { + messages: vec![], + subscriptions: vec![], + control_msgs: vec![ControlAction::IDontWant(IDontWant { + message_ids: vec![msg_id.clone()], + })], + }; + gs.on_connection_handler_event( + peers[1], + ConnectionId::new_unchecked(0), + HandlerEvent::Message { + rpc, + invalid_messages: vec![], + }, + ); + + // Receive IWANT from Peer 1. + gs.handle_iwant(&peers[1], vec![msg_id.clone()]); + + // Check that no messages are sent. + queues.iter().for_each(|(_, receiver_queue)| { + assert!(receiver_queue.is_empty()); + }); +} + +/// tests that an event is created when a peer shares that it has a message we want +#[test] +fn test_handle_ihave_subscribed_and_msg_not_cached() { + let (mut gs, peers, mut queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + + gs.handle_ihave( + &peers[7], + vec![(topic_hashes[0].clone(), vec![MessageId::new(b"unknown id")])], + ); + + // check that we sent an IWANT request for `unknown id` + let mut iwant_exists = false; + let mut receiver_queue = queues.remove(&peers[7]).unwrap(); + while !receiver_queue.is_empty() { + if let Some(RpcOut::IWant(IWant { message_ids })) = receiver_queue.try_pop() { + if message_ids + .iter() + .any(|m| *m == MessageId::new(b"unknown id")) + { + iwant_exists = true; + break; + } + } + } + + assert!( + iwant_exists, + "Expected to send an IWANT control message for unknown message id" + ); +} + +/// tests that an event is not created when a peer shares that it has a message that +/// we already have +#[test] +fn test_handle_ihave_subscribed_and_msg_cached() { + let (mut gs, peers, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + + let msg_id = MessageId::new(b"known id"); + + let events_before = gs.events.len(); + gs.handle_ihave(&peers[7], vec![(topic_hashes[0].clone(), vec![msg_id])]); + let events_after = gs.events.len(); + + assert_eq!( + events_before, events_after, + "Expected event count to stay the same" + ) +} + +/// test that an event is not created when a peer shares that it has a message in +/// a topic that we are not subscribed to +#[test] +fn test_handle_ihave_not_subscribed() { + let (mut gs, peers, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(vec![]) + .to_subscribe(true) + .create_network(); + + let events_before = gs.events.len(); + gs.handle_ihave( + &peers[7], + vec![( + TopicHash::from_raw(String::from("unsubscribed topic")), + vec![MessageId::new(b"irrelevant id")], + )], + ); + let events_after = gs.events.len(); + + assert_eq!( + events_before, events_after, + "Expected event count to stay the same" + ) +} + +#[test] +fn test_gossip_to_at_least_gossip_lazy_peers() { + let config: Config = Config::default(); + + // add more peers than in mesh to test gossipping + // by default only mesh_n_low peers will get added to mesh + let (mut gs, _, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_low() + config.gossip_lazy() + 1) + .topics(vec!["topic".into()]) + .to_subscribe(true) + .create_network(); + + // receive message + let raw_message = RawMessage { + source: Some(PeerId::random()), + data: vec![], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(raw_message.clone(), &PeerId::random()); + + // emit gossip + gs.emit_gossip(); + + // Transform the inbound message + let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); + + let msg_id = gs.config.message_id(message); + + // check that exactly config.gossip_lazy() many gossip messages were sent. + let (control_msgs, _) = count_control_msgs(queues, |_, action| match action { + RpcOut::IHave(IHave { + topic_hash, + message_ids, + }) => topic_hash == &topic_hashes[0] && message_ids.iter().any(|id| id == &msg_id), + _ => false, + }); + assert_eq!(control_msgs, config.gossip_lazy()); +} + +#[test] +fn test_gossip_to_at_most_gossip_factor_peers() { + let config: Config = Config::default(); + + // add a lot of peers + let m = config.mesh_n_low() + config.gossip_lazy() * (2.0 / config.gossip_factor()) as usize; + let (mut gs, _, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(m) + .topics(vec!["topic".into()]) + .to_subscribe(true) + .create_network(); + + // receive message + let raw_message = RawMessage { + source: Some(PeerId::random()), + data: vec![], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(raw_message.clone(), &PeerId::random()); + + // emit gossip + gs.emit_gossip(); + + // Transform the inbound message + let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); + + let msg_id = gs.config.message_id(message); + // check that exactly config.gossip_lazy() many gossip messages were sent. + let (control_msgs, _) = count_control_msgs(queues, |_, action| match action { + RpcOut::IHave(IHave { + topic_hash, + message_ids, + }) => topic_hash == &topic_hashes[0] && message_ids.iter().any(|id| id == &msg_id), + _ => false, + }); + assert_eq!( + control_msgs, + ((m - config.mesh_n_low()) as f64 * config.gossip_factor()) as usize + ); +} + +#[test] +fn test_ignore_too_many_iwants_from_same_peer_for_same_message() { + let config = Config::default(); + // build gossipsub with full mesh + let (mut gs, _, mut queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(false) + .create_network(); + + // add another peer not in the mesh + let (peer, queue) = add_peer(&mut gs, &topics, false, false); + queues.insert(peer, queue); + + // receive a message + let mut seq = 0; + let m1 = random_message(&mut seq, &topics); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1.clone()).unwrap(); + + let id = config.message_id(message1); + + gs.handle_received_message(m1, &PeerId::random()); + + // clear events + let queues = flush_events(&mut gs, queues); + + // the first gossip_retransimission many iwants return the valid message, all others are + // ignored. + for _ in 0..(2 * config.gossip_retransimission() + 10) { + gs.handle_iwant(&peer, vec![id.clone()]); + } + + assert_eq!( + queues.into_values().fold(0, |mut fwds, mut queue| { + while !queue.is_empty() { + if let Some(RpcOut::Forward { .. }) = queue.try_pop() { + fwds += 1; + } + } + fwds + }), + config.gossip_retransimission() as usize, + "not more then gossip_retransmission many messages get sent back" + ); +} + +#[test] +fn test_ignore_too_many_ihaves() { + let config = ConfigBuilder::default() + .max_ihave_messages(10) + .build() + .unwrap(); + // build gossipsub with full mesh + let (mut gs, _, mut queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config.clone()) + .create_network(); + + // add another peer not in the mesh + let (peer, queue) = add_peer(&mut gs, &topics, false, false); + queues.insert(peer, queue); + + // peer has 20 messages + let mut seq = 0; + let messages: Vec<_> = (0..20).map(|_| random_message(&mut seq, &topics)).collect(); + + // peer sends us one ihave for each message in order + for raw_message in &messages { + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform(raw_message.clone()) + .unwrap(); + + gs.handle_ihave( + &peer, + vec![(topics[0].clone(), vec![config.message_id(message)])], + ); + } + + let first_ten: HashSet<_> = messages + .iter() + .take(10) + .map(|msg| gs.data_transform.inbound_transform(msg.clone()).unwrap()) + .map(|m| config.message_id(&m)) + .collect(); + + // we send iwant only for the first 10 messages + let (control_msgs, queues) = count_control_msgs(queues, |p, action| { + p == &peer + && matches!(action, RpcOut::IWant(IWant { message_ids }) if message_ids.len() == 1 && first_ten.contains(&message_ids[0])) + }); + assert_eq!( + control_msgs, 10, + "exactly the first ten ihaves should be processed and one iwant for each created" + ); + + // after a heartbeat everything is forgotten + gs.heartbeat(); + + for raw_message in messages[10..].iter() { + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform(raw_message.clone()) + .unwrap(); + + gs.handle_ihave( + &peer, + vec![(topics[0].clone(), vec![config.message_id(message)])], + ); + } + + // we sent iwant for all 10 messages + let (control_msgs, _) = count_control_msgs(queues, |p, action| { + p == &peer + && matches!(action, RpcOut::IWant(IWant { message_ids }) if message_ids.len() == 1) + }); + assert_eq!(control_msgs, 10, "all 20 should get sent"); +} + +#[test] +fn test_ignore_too_many_messages_in_ihave() { + let config = ConfigBuilder::default() + .max_ihave_messages(10) + .max_ihave_length(10) + .build() + .unwrap(); + // build gossipsub with full mesh + let (mut gs, _, mut queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config.clone()) + .create_network(); + + // add another peer not in the mesh + let (peer, queue) = add_peer(&mut gs, &topics, false, false); + queues.insert(peer, queue); + + // peer has 30 messages + let mut seq = 0; + let message_ids: Vec<_> = (0..30) + .map(|_| random_message(&mut seq, &topics)) + .map(|msg| gs.data_transform.inbound_transform(msg).unwrap()) + .map(|msg| config.message_id(&msg)) + .collect(); + + // peer sends us three ihaves + gs.handle_ihave(&peer, vec![(topics[0].clone(), message_ids[0..8].to_vec())]); + gs.handle_ihave( + &peer, + vec![(topics[0].clone(), message_ids[0..12].to_vec())], + ); + gs.handle_ihave( + &peer, + vec![(topics[0].clone(), message_ids[0..20].to_vec())], + ); + + let first_twelve: HashSet<_> = message_ids.iter().take(12).collect(); + + // we send iwant only for the first 10 messages + let mut sum = 0; + let (control_msgs, queues) = count_control_msgs(queues, |p, rpc| match rpc { + RpcOut::IWant(IWant { message_ids }) => { + p == &peer && { + assert!(first_twelve.is_superset(&message_ids.iter().collect())); + sum += message_ids.len(); + true + } + } + _ => false, + }); + assert_eq!( + control_msgs, 2, + "the third ihave should get ignored and no iwant sent" + ); + + assert_eq!(sum, 10, "exactly the first ten ihaves should be processed"); + + // after a heartbeat everything is forgotten + gs.heartbeat(); + gs.handle_ihave( + &peer, + vec![(topics[0].clone(), message_ids[20..30].to_vec())], + ); + + // we sent 10 iwant messages ids via a IWANT rpc. + let mut sum = 0; + let (control_msgs, _) = count_control_msgs(queues, |p, rpc| match rpc { + RpcOut::IWant(IWant { message_ids }) => { + p == &peer && { + sum += message_ids.len(); + true + } + } + _ => false, + }); + assert_eq!(control_msgs, 1); + assert_eq!(sum, 10, "exactly 20 iwants should get sent"); +} + +#[test] +fn test_limit_number_of_message_ids_inside_ihave() { + let config = ConfigBuilder::default() + .max_ihave_messages(10) + .max_ihave_length(100) + .build() + .unwrap(); + // build gossipsub with full mesh + let (mut gs, peers, mut queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config) + .create_network(); + + // graft to all peers to really fill the mesh with all the peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // add two other peers not in the mesh + let (p1, queue1) = add_peer(&mut gs, &topics, false, false); + queues.insert(p1, queue1); + let (p2, queue2) = add_peer(&mut gs, &topics, false, false); + queues.insert(p2, queue2); + + // receive 200 messages from another peer + let mut seq = 0; + for _ in 0..200 { + gs.handle_received_message(random_message(&mut seq, &topics), &PeerId::random()); + } + + // emit gossip + gs.emit_gossip(); + + // both peers should have gotten 100 random ihave messages, to assert the randomness, we + // assert that both have not gotten the same set of messages, but have an intersection + // (which is the case with very high probability, the probabiltity of failure is < 10^-58). + + let mut ihaves1 = HashSet::new(); + let mut ihaves2 = HashSet::new(); + + let (control_msgs, _) = count_control_msgs(queues, |p, action| match action { + RpcOut::IHave(IHave { message_ids, .. }) => { + if p == &p1 { + ihaves1 = message_ids.iter().cloned().collect(); + true + } else if p == &p2 { + ihaves2 = message_ids.iter().cloned().collect(); + true + } else { + false + } + } + _ => false, + }); + assert_eq!( + control_msgs, 2, + "should have emitted one ihave to p1 and one to p2" + ); + + assert_eq!( + ihaves1.len(), + 100, + "should have sent 100 message ids in ihave to p1" + ); + assert_eq!( + ihaves2.len(), + 100, + "should have sent 100 message ids in ihave to p2" + ); + assert!( + ihaves1 != ihaves2, + "should have sent different random messages to p1 and p2 \ + (this may fail with a probability < 10^-58" + ); + assert!( + ihaves1.intersection(&ihaves2).count() > 0, + "should have sent random messages with some common messages to p1 and p2 \ + (this may fail with a probability < 10^-58" + ); +} + +#[test] +fn test_iwant_penalties() { + let config = ConfigBuilder::default() + .iwant_followup_time(Duration::from_secs(4)) + .build() + .unwrap(); + let peer_score_params = PeerScoreParams { + behaviour_penalty_weight: -1.0, + ..Default::default() + }; + + // fill the mesh + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) + .create_network(); + + // graft to all peers to really fill the mesh with all the peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // add 100 more peers + let other_peers: Vec<_> = (0..100) + .map(|_| add_peer(&mut gs, &topics, false, false)) + .collect(); + + // each peer sends us an ihave containing each two message ids + let mut first_messages = Vec::new(); + let mut second_messages = Vec::new(); + let mut seq = 0; + for (peer, _queue) in &other_peers { + let msg1 = random_message(&mut seq, &topics); + let msg2 = random_message(&mut seq, &topics); + + // Decompress the raw message and calculate the message id. + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(msg1.clone()).unwrap(); + + // Transform the inbound message + let message2 = &gs.data_transform.inbound_transform(msg2.clone()).unwrap(); + + first_messages.push(msg1.clone()); + second_messages.push(msg2.clone()); + gs.handle_ihave( + peer, + vec![( + topics[0].clone(), + vec![config.message_id(message1), config.message_id(message2)], + )], + ); + } + + // the peers send us all the first message ids in time + for (index, (peer, _queue)) in other_peers.iter().enumerate() { + gs.handle_received_message(first_messages[index].clone(), peer); + } + + // now we do a heartbeat no penalization should have been applied yet + gs.heartbeat(); + + for (peer, _queue) in &other_peers { + assert_eq!(gs.as_peer_score_mut().score_report(peer).score, 0.0); + } + + // receive the first twenty of the other peers then send their response + for (index, (peer, _queue)) in other_peers.iter().enumerate().take(20) { + gs.handle_received_message(second_messages[index].clone(), peer); + } + + // sleep for the promise duration + sleep(Duration::from_secs(4)); + + // now we do a heartbeat to apply penalization + gs.heartbeat(); + + // now we get the second messages from the last 80 peers. + for (index, (peer, _queue)) in other_peers.iter().enumerate() { + if index > 19 { + gs.handle_received_message(second_messages[index].clone(), peer); + } + } + + // no further penalizations should get applied + gs.heartbeat(); + + // Only the last 80 peers should be penalized for not responding in time + let mut not_penalized = 0; + let mut single_penalized = 0; + let mut double_penalized = 0; + + for (i, (peer, _queue)) in other_peers.iter().enumerate() { + let score = gs.as_peer_score_mut().score_report(peer).score; + if score == 0.0 { + not_penalized += 1; + } else if score == -1.0 { + assert!(i > 9); + single_penalized += 1; + } else if score == -4.0 { + assert!(i > 9); + double_penalized += 1 + } else { + println!("{peer}"); + println!("{score}"); + panic!("Invalid score of peer"); + } + } + + assert_eq!(not_penalized, 20); + assert_eq!(single_penalized, 80); + assert_eq!(double_penalized, 0); +} diff --git a/protocols/gossipsub/src/behaviour/tests/graft_prune.rs b/protocols/gossipsub/src/behaviour/tests/graft_prune.rs new file mode 100644 index 00000000000..2c967673228 --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/graft_prune.rs @@ -0,0 +1,507 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Tests for GRAFT/PRUNE handling, backoff, and peer exchange. + +use std::{ + collections::{HashMap, HashSet}, + thread::sleep, + time::Duration, +}; + +use libp2p_identity::PeerId; +use libp2p_swarm::ToSwarm; + +use super::{count_control_msgs, disconnect_peer, flush_events, DefaultBehaviourTestBuilder}; +use crate::{ + config::{Config, ConfigBuilder}, + topic::TopicHash, + types::{PeerInfo, Prune, RpcOut}, + IdentTopic as Topic, +}; + +/// tests that a peer is added to our mesh when we are both subscribed +/// to the same topic +#[test] +fn test_handle_graft_is_subscribed() { + let (mut gs, peers, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + + gs.handle_graft(&peers[7], topic_hashes.clone()); + + assert!( + gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), + "Expected peer to have been added to mesh" + ); +} + +/// tests that a peer is not added to our mesh when they are subscribed to +/// a topic that we are not +#[test] +fn test_handle_graft_is_not_subscribed() { + let (mut gs, peers, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + + gs.handle_graft( + &peers[7], + vec![TopicHash::from_raw(String::from("unsubscribed topic"))], + ); + + assert!( + !gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), + "Expected peer to have been added to mesh" + ); +} + +/// tests multiple topics in a single graft message +#[test] +fn test_handle_graft_multiple_topics() { + let topics: Vec = ["topic1", "topic2", "topic3", "topic4"] + .iter() + .map(|&t| String::from(t)) + .collect(); + + let (mut gs, peers, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(topics) + .to_subscribe(true) + .create_network(); + + let mut their_topics = topic_hashes.clone(); + // their_topics = [topic1, topic2, topic3] + // our_topics = [topic1, topic2, topic4] + their_topics.pop(); + gs.leave(&their_topics[2]); + + gs.handle_graft(&peers[7], their_topics.clone()); + + for hash in topic_hashes.iter().take(2) { + assert!( + gs.mesh.get(hash).unwrap().contains(&peers[7]), + "Expected peer to be in the mesh for the first 2 topics" + ); + } + + assert!( + !gs.mesh.contains_key(&topic_hashes[2]), + "Expected the second topic to not be in the mesh" + ); +} + +/// tests that a peer is removed from our mesh +#[test] +fn test_handle_prune_peer_in_mesh() { + let (mut gs, peers, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + + // insert peer into our mesh for 'topic1' + gs.mesh + .insert(topic_hashes[0].clone(), peers.iter().cloned().collect()); + assert!( + gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), + "Expected peer to be in mesh" + ); + + gs.handle_prune( + &peers[7], + topic_hashes + .iter() + .map(|h| (h.clone(), vec![], None)) + .collect(), + ); + assert!( + !gs.mesh.get(&topic_hashes[0]).unwrap().contains(&peers[7]), + "Expected peer to be removed from mesh" + ); +} + +#[test] +fn test_connect_to_px_peers_on_handle_prune() { + let config: Config = Config::default(); + + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + // handle prune from single peer with px peers + + let mut px = Vec::new(); + // propose more px peers than config.prune_peers() + for _ in 0..config.prune_peers() + 5 { + px.push(PeerInfo { + peer_id: Some(PeerId::random()), + }); + } + + gs.handle_prune( + &peers[0], + vec![( + topics[0].clone(), + px.clone(), + Some(config.prune_backoff().as_secs()), + )], + ); + + // Check DialPeer events for px peers + let dials: Vec<_> = gs + .events + .iter() + .filter_map(|e| match e { + ToSwarm::Dial { opts } => opts.get_peer_id(), + _ => None, + }) + .collect(); + + // Exactly config.prune_peers() many random peers should be dialled + assert_eq!(dials.len(), config.prune_peers()); + + let dials_set: HashSet<_> = dials.into_iter().collect(); + + // No duplicates + assert_eq!(dials_set.len(), config.prune_peers()); + + // all dial peers must be in px + assert!(dials_set.is_subset( + &px.iter() + .map(|i| *i.peer_id.as_ref().unwrap()) + .collect::>() + )); +} + +#[test] +fn test_send_px_and_backoff_in_prune() { + let config: Config = Config::default(); + + // build mesh with enough peers for px + let (mut gs, peers, queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.prune_peers() + 1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + // send prune to peer + gs.send_graft_prune( + HashMap::new(), + vec![(peers[0], vec![topics[0].clone()])] + .into_iter() + .collect(), + HashSet::new(), + ); + + // check prune message + let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| { + peer_id == &peers[0] + && match m { + RpcOut::Prune(Prune { + topic_hash, + peers, + backoff, + }) => { + topic_hash == &topics[0] && + peers.len() == config.prune_peers() && + //all peers are different + peers.iter().collect::>().len() == + config.prune_peers() && + backoff.unwrap() == config.prune_backoff().as_secs() + } + _ => false, + } + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_prune_backoffed_peer_on_graft() { + let config: Config = Config::default(); + + // build mesh with enough peers for px + let (mut gs, peers, queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.prune_peers() + 1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + // remove peer from mesh and send prune to peer => this adds a backoff for this peer + gs.mesh.get_mut(&topics[0]).unwrap().remove(&peers[0]); + gs.send_graft_prune( + HashMap::new(), + vec![(peers[0], vec![topics[0].clone()])] + .into_iter() + .collect(), + HashSet::new(), + ); + + // ignore all messages until now + let queues = flush_events(&mut gs, queues); + + // handle graft + gs.handle_graft(&peers[0], vec![topics[0].clone()]); + + // check prune message + let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| { + peer_id == &peers[0] + && match m { + RpcOut::Prune(Prune { + topic_hash, + peers, + backoff, + }) => { + topic_hash == &topics[0] && + //no px in this case + peers.is_empty() && + backoff.unwrap() == config.prune_backoff().as_secs() + } + _ => false, + } + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_do_not_graft_within_backoff_period() { + let config = ConfigBuilder::default() + .backoff_slack(1) + .heartbeat_interval(Duration::from_millis(100)) + .build() + .unwrap(); + // only one peer => mesh too small and will try to regraft as early as possible + let (mut gs, peers, queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + // handle prune from peer with backoff of one second + gs.handle_prune(&peers[0], vec![(topics[0].clone(), Vec::new(), Some(1))]); + + // forget all events until now + let queues = flush_events(&mut gs, queues); + + // call heartbeat + gs.heartbeat(); + + // Sleep for one second and apply 10 regular heartbeats (interval = 100ms). + for _ in 0..10 { + sleep(Duration::from_millis(100)); + gs.heartbeat(); + } + + // Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat + // is needed). + let (control_msgs, queues) = + count_control_msgs(queues, |_, m| matches!(m, RpcOut::Graft { .. })); + assert_eq!( + control_msgs, 0, + "Graft message created too early within backoff period" + ); + + // Heartbeat one more time this should graft now + sleep(Duration::from_millis(100)); + gs.heartbeat(); + + // check that graft got created + let (control_msgs, _) = count_control_msgs(queues, |_, m| matches!(m, RpcOut::Graft { .. })); + assert!( + control_msgs > 0, + "No graft message was created after backoff period" + ); +} + +#[test] +fn test_do_not_graft_within_default_backoff_period_after_receiving_prune_without_backoff() { + // set default backoff period to 1 second + let config = ConfigBuilder::default() + .prune_backoff(Duration::from_millis(90)) + .backoff_slack(1) + .heartbeat_interval(Duration::from_millis(100)) + .build() + .unwrap(); + // only one peer => mesh too small and will try to regraft as early as possible + let (mut gs, peers, queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + // handle prune from peer without a specified backoff + gs.handle_prune(&peers[0], vec![(topics[0].clone(), Vec::new(), None)]); + + // forget all events until now + let queues = flush_events(&mut gs, queues); + + // call heartbeat + gs.heartbeat(); + + // Apply one more heartbeat + sleep(Duration::from_millis(100)); + gs.heartbeat(); + + // Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat + // is needed). + let (control_msgs, queues) = + count_control_msgs(queues, |_, m| matches!(m, RpcOut::Graft { .. })); + assert_eq!( + control_msgs, 0, + "Graft message created too early within backoff period" + ); + + // Heartbeat one more time this should graft now + sleep(Duration::from_millis(100)); + gs.heartbeat(); + + // check that graft got created + let (control_msgs, _) = count_control_msgs(queues, |_, m| matches!(m, RpcOut::Graft { .. })); + assert!( + control_msgs > 0, + "No graft message was created after backoff period" + ); +} + +#[test] +fn test_unsubscribe_backoff() { + const HEARTBEAT_INTERVAL: Duration = Duration::from_millis(100); + let config = ConfigBuilder::default() + .backoff_slack(1) + // ensure a prune_backoff > unsubscribe_backoff + .prune_backoff(Duration::from_secs(5)) + .unsubscribe_backoff(Duration::from_secs(1)) + .heartbeat_interval(HEARTBEAT_INTERVAL) + .build() + .unwrap(); + + let topic = String::from("test"); + // only one peer => mesh too small and will try to regraft as early as possible + let (mut gs, _, queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec![topic.clone()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + let _ = gs.unsubscribe(&Topic::new(topic)); + + let (control_msgs, queues) = count_control_msgs(queues, |_, m| match m { + RpcOut::Prune(Prune { backoff, .. }) => backoff == &Some(1), + _ => false, + }); + assert_eq!( + control_msgs, 1, + "Peer should be pruned with `unsubscribe_backoff`." + ); + + let _ = gs.subscribe(&Topic::new(topics[0].to_string())); + + // forget all events until now + let queues = flush_events(&mut gs, queues); + + // call heartbeat + gs.heartbeat(); + + // Sleep for one second and apply 10 regular heartbeats (interval = 100ms). + for _ in 0..10 { + sleep(HEARTBEAT_INTERVAL); + gs.heartbeat(); + } + + // Check that no graft got created (we have backoff_slack = 1 therefore one more heartbeat + // is needed). + let (control_msgs, queues) = + count_control_msgs(queues, |_, m| matches!(m, RpcOut::Graft { .. })); + assert_eq!( + control_msgs, 0, + "Graft message created too early within backoff period" + ); + + // Heartbeat one more time this should graft now + sleep(HEARTBEAT_INTERVAL); + gs.heartbeat(); + + // check that graft got created + let (control_msgs, _) = count_control_msgs(queues, |_, m| matches!(m, RpcOut::Graft { .. })); + assert!( + control_msgs > 0, + "No graft message was created after backoff period" + ); +} + +#[test] +fn test_ignore_graft_from_unknown_topic() { + let (mut gs, peers, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec![]) + .to_subscribe(true) + .create_network(); + + gs.handle_graft( + &peers[0], + vec![TopicHash::from_raw(String::from("unknown"))], + ); + + assert!( + !gs.mesh + .contains_key(&TopicHash::from_raw(String::from("unknown"))), + "Unknown topic should not be added to mesh" + ); +} + +#[test] +/// Test nodes that send grafts without subscriptions. +fn test_graft_without_subscribe() { + // The node should: + // - Create an empty vector in mesh[topic] + // - Send subscription request to all peers + // - run JOIN(topic) + + let topic = String::from("test_subscribe"); + let subscribe_topic = vec![topic.clone()]; + let subscribe_topic_hash = vec![Topic::new(topic.clone()).hash()]; + let (mut gs, peers, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(subscribe_topic) + .to_subscribe(false) + .create_network(); + + assert!( + gs.mesh.contains_key(&topic_hashes[0]), + "Subscribe should add a new entry to the mesh[topic] hashmap" + ); + + // The node sends a graft for the subscribe topic. + gs.handle_graft(&peers[0], subscribe_topic_hash); + + // The node disconnects + disconnect_peer(&mut gs, &peers[0]); + + // We unsubscribe from the topic. + let _ = gs.unsubscribe(&Topic::new(topic)); +} diff --git a/protocols/gossipsub/src/behaviour/tests/idontwant.rs b/protocols/gossipsub/src/behaviour/tests/idontwant.rs new file mode 100644 index 00000000000..51a3e63c698 --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/idontwant.rs @@ -0,0 +1,265 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Tests for IDONTWANT message handling. + +use std::time::Instant; + +use libp2p_identity::PeerId; +use libp2p_swarm::{ConnectionId, NetworkBehaviour}; + +use super::DefaultBehaviourTestBuilder; +use crate::{ + config::Config, + handler::HandlerEvent, + transform::DataTransform, + types::{ControlAction, IDontWant, MessageId, PeerKind, RawMessage, RpcIn, RpcOut}, +}; + +/// Test that a node sends IDONTWANT messages to mesh peers +/// that run Gossipsub v1.2. +#[test] +fn sends_idontwant() { + let (mut gs, peers, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(5) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .peer_kind(PeerKind::Gossipsubv1_2) + .create_network(); + + let local_id = PeerId::random(); + + let message = RawMessage { + source: Some(peers[1]), + data: vec![12u8; 1024], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(message.clone(), &local_id); + assert_eq!( + queues + .into_iter() + .fold(0, |mut idontwants, (peer_id, mut queue)| { + while !queue.is_empty() { + if let Some(RpcOut::IDontWant(_)) = queue.try_pop() { + assert_ne!(peer_id, peers[1]); + idontwants += 1; + } + } + idontwants + }), + 3, + "IDONTWANT was not sent" + ); +} + +#[test] +fn doesnt_sends_idontwant_for_lower_message_size() { + let (mut gs, peers, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(5) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .peer_kind(PeerKind::Gossipsubv1_2) + .create_network(); + + let local_id = PeerId::random(); + + let message = RawMessage { + source: Some(peers[1]), + data: vec![12], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + + gs.handle_received_message(message.clone(), &local_id); + assert_eq!( + queues + .into_iter() + .fold(0, |mut idontwants, (peer_id, mut queue)| { + while !queue.is_empty() { + if let Some(RpcOut::IDontWant(_)) = queue.try_pop() { + assert_ne!(peer_id, peers[1]); + idontwants += 1; + } + } + idontwants + }), + 0, + "IDONTWANT was sent" + ); +} + +/// Test that a node doesn't send IDONTWANT messages to the mesh peers +/// that don't run Gossipsub v1.2. +#[test] +fn doesnt_send_idontwant() { + let (mut gs, peers, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(5) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .peer_kind(PeerKind::Gossipsubv1_1) + .create_network(); + + let local_id = PeerId::random(); + + let message = RawMessage { + source: Some(peers[1]), + data: vec![12], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(message.clone(), &local_id); + assert_eq!( + queues + .into_iter() + .fold(0, |mut idontwants, (peer_id, mut queue)| { + while !queue.is_empty() { + if matches!(queue.try_pop(), Some(RpcOut::IDontWant(_)) if peer_id != peers[1]) + { + idontwants += 1; + } + } + idontwants + }), + 0, + "IDONTWANT were sent" + ); +} + +/// Test that a node doesn't forward a messages to the mesh peers +/// that sent IDONTWANT. +#[test] +fn doesnt_forward_idontwant() { + let (mut gs, peers, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(4) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .peer_kind(PeerKind::Gossipsubv1_2) + .create_network(); + + let local_id = PeerId::random(); + + let raw_message = RawMessage { + source: Some(peers[1]), + data: vec![12], + sequence_number: Some(0), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: true, + }; + let message = gs + .data_transform + .inbound_transform(raw_message.clone()) + .unwrap(); + let message_id = gs.config.message_id(&message); + let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); + peer.dont_send.insert(message_id, Instant::now()); + + gs.handle_received_message(raw_message.clone(), &local_id); + assert_eq!( + queues + .into_iter() + .fold(0, |mut fwds, (peer_id, mut queue)| { + while !queue.is_empty() { + if let Some(RpcOut::Forward { .. }) = queue.try_pop() { + assert_ne!(peer_id, peers[2]); + fwds += 1; + } + } + fwds + }), + 2, + "IDONTWANT was not sent" + ); +} + +/// Test that a node parses an +/// IDONTWANT message to the respective peer. +#[test] +fn parses_idontwant() { + let (mut gs, peers, _queues, _topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .peer_kind(PeerKind::Gossipsubv1_2) + .create_network(); + + let message_id = MessageId::new(&[0, 1, 2, 3]); + let rpc = RpcIn { + messages: vec![], + subscriptions: vec![], + control_msgs: vec![ControlAction::IDontWant(IDontWant { + message_ids: vec![message_id.clone()], + })], + }; + gs.on_connection_handler_event( + peers[1], + ConnectionId::new_unchecked(0), + HandlerEvent::Message { + rpc, + invalid_messages: vec![], + }, + ); + let peer = gs.connected_peers.get_mut(&peers[1]).unwrap(); + assert!(peer.dont_send.get(&message_id).is_some()); +} + +/// Test that a node clears stale IDONTWANT messages. +#[test] +fn clear_stale_idontwant() { + use std::time::Duration; + + let (mut gs, peers, _queues, _topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(4) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(1) + .peer_kind(PeerKind::Gossipsubv1_2) + .create_network(); + + let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); + peer.dont_send + .insert(MessageId::new(&[1, 2, 3, 4]), Instant::now()); + std::thread::sleep(Duration::from_secs(3)); + gs.heartbeat(); + let peer = gs.connected_peers.get_mut(&peers[2]).unwrap(); + assert!(peer.dont_send.is_empty()); +} diff --git a/protocols/gossipsub/src/behaviour/tests/mesh.rs b/protocols/gossipsub/src/behaviour/tests/mesh.rs new file mode 100644 index 00000000000..796f19ea309 --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/mesh.rs @@ -0,0 +1,183 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Tests for mesh management (addition, subtraction, maintenance). + +use std::collections::BTreeSet; + +use hashlink::LinkedHashMap; +use libp2p_identity::PeerId; +use libp2p_swarm::ConnectionId; + +use super::DefaultBehaviourTestBuilder; +use crate::{ + behaviour::{get_random_peers, Behaviour, MessageAuthenticity}, + config::{Config, ConfigBuilder, ValidationMode}, + queue::Queue, + types::{PeerDetails, PeerKind}, + IdentTopic as Topic, +}; + +/// Tests the mesh maintenance addition +#[test] +fn test_mesh_addition() { + let config: Config = Config::default(); + + // Adds mesh_low peers and PRUNE 2 giving us a deficit. + let (mut gs, peers, _queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n() + 1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .create_network(); + + let to_remove_peers = config.mesh_n() + 1 - config.mesh_n_low() - 1; + + for peer in peers.iter().take(to_remove_peers) { + gs.handle_prune( + peer, + topics.iter().map(|h| (h.clone(), vec![], None)).collect(), + ); + } + + // Verify the pruned peers are removed from the mesh. + assert_eq!( + gs.mesh.get(&topics[0]).unwrap().len(), + config.mesh_n_low() - 1 + ); + + // run a heartbeat + gs.heartbeat(); + + // Peers should be added to reach mesh_n + assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), config.mesh_n()); +} + +/// Tests the mesh maintenance subtraction +#[test] +fn test_mesh_subtraction() { + let config = Config::default(); + + // Adds mesh_low peers and PRUNE 2 giving us a deficit. + let n = config.mesh_n_high() + 10; + // make all outbound connections so that we allow grafting to all + let (mut gs, peers, _queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(n) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .outbound(n) + .create_network(); + + // graft all the peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // run a heartbeat + gs.heartbeat(); + + // Peers should be removed to reach mesh_n + assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), config.mesh_n()); +} + +#[test] +fn test_do_not_remove_too_many_outbound_peers() { + let config = Config::default(); + + // Adds mesh_low peers and PRUNE 2 giving us a deficit. + let n = config.mesh_n_high() + 10; + // make all outbound connections so that we allow grafting to all + let (mut gs, peers, _queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(n) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .outbound(n) + .create_network(); + + // graft all the peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // run a heartbeat + gs.heartbeat(); + + // Peers should be removed to reach mesh_n + assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), config.mesh_n()); +} + +/// Test Gossipsub.get_random_peers() function +#[test] +fn test_get_random_peers() { + // generate a default Config + let gs_config = ConfigBuilder::default() + .validation_mode(ValidationMode::Anonymous) + .build() + .unwrap(); + // create a gossipsub struct + let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::Anonymous, gs_config).unwrap(); + + // create a topic and fill it with some peers + let topic_hash = Topic::new("Test").hash(); + let mut peers = vec![]; + let mut topics = BTreeSet::new(); + topics.insert(topic_hash.clone()); + + for _ in 0..20 { + let peer_id = PeerId::random(); + peers.push(peer_id); + gs.connected_peers.insert( + peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(gs.config.connection_handler_queue_len()), + dont_send: LinkedHashMap::new(), + }, + ); + } + + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 5, |_| true); + assert_eq!(random_peers.len(), 5, "Expected 5 peers to be returned"); + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 30, |_| true); + assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); + assert!( + random_peers == peers.iter().cloned().collect(), + "Expected no shuffling" + ); + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 20, |_| true); + assert!(random_peers.len() == 20, "Expected 20 peers to be returned"); + assert!( + random_peers == peers.iter().cloned().collect(), + "Expected no shuffling" + ); + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 0, |_| true); + assert!(random_peers.is_empty(), "Expected 0 peers to be returned"); + // test the filter + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 5, |_| false); + assert!(random_peers.is_empty(), "Expected 0 peers to be returned"); + let random_peers = get_random_peers(&gs.connected_peers, &topic_hash, 10, { + |peer| peers.contains(peer) + }); + assert!(random_peers.len() == 10, "Expected 10 peers to be returned"); +} diff --git a/protocols/gossipsub/src/behaviour/tests/mod.rs b/protocols/gossipsub/src/behaviour/tests/mod.rs new file mode 100644 index 00000000000..83ba4e0166d --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/mod.rs @@ -0,0 +1,613 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Test utilities and infrastructure for gossipsub behaviour tests. +//! +//! This module provides shared test utilities used across all gossipsub behaviour test modules. +//! The main components are: +//! +//! - [`BehaviourTestBuilder`]: A builder for creating test network configurations with peers, +//! topics, and various gossipsub settings. +//! - Peer management functions: [`add_peer`], [`add_peer_with_addr`], +//! [`add_peer_with_addr_and_kind`], [`disconnect_peer`] +//! - Message utilities: [`proto_to_message`], [`random_message`] +//! - Event helpers: [`count_control_msgs`], [`flush_events`] + +mod explicit_peers; +mod floodsub; +mod gossip; +mod graft_prune; +mod idontwant; +mod mesh; +mod peer_queues; +mod publish; +mod scoring; +mod subscription; +mod topic_config; + +use std::collections::HashMap; + +use byteorder::{BigEndian, ByteOrder}; +use hashlink::LinkedHashMap; +use libp2p_core::ConnectedPoint; +use rand::Rng; + +use super::*; +use crate::{types::RpcIn, IdentTopic as Topic}; + +/// Convenience alias for [`BehaviourTestBuilder`] with default transform and subscription filter. +pub(super) type DefaultBehaviourTestBuilder = + BehaviourTestBuilder; + +/// A builder for creating test gossipsub networks with configurable peers and topics. +/// +/// This struct uses the builder pattern to configure a test network setup. +/// Call [`create_network`](Self::create_network) to finalize and create the network. +/// +/// # Example +/// +/// ```ignore +/// let (gs, peers, queues, topics) = DefaultBehaviourTestBuilder::default() +/// .peer_no(20) +/// .topics(vec!["topic1".into()]) +/// .to_subscribe(true) +/// .gs_config(Config::default()) +/// .create_network(); +/// ``` +#[derive(Default, Debug)] +pub(super) struct BehaviourTestBuilder { + peer_no: usize, + topics: Vec, + to_subscribe: bool, + gs_config: Config, + explicit: usize, + outbound: usize, + scoring: Option<(PeerScoreParams, PeerScoreThresholds)>, + data_transform: D, + subscription_filter: F, + peer_kind: Option, +} + +impl BehaviourTestBuilder +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + /// Creates the test network with the configured settings. + /// + /// # Returns + /// + /// A tuple containing: + /// - `Behaviour`: The gossipsub behaviour instance + /// - `Vec`: List of connected peer IDs + /// - `HashMap`: Message queues for each peer (for inspecting sent messages) + /// - `Vec`: List of subscribed topic hashes + #[allow(clippy::type_complexity)] + pub(super) fn create_network( + self, + ) -> ( + Behaviour, + Vec, + HashMap, + Vec, + ) { + let keypair = libp2p_identity::Keypair::generate_ed25519(); + // create a gossipsub struct + let mut gs: Behaviour = Behaviour::new_with_subscription_filter_and_transform( + MessageAuthenticity::Signed(keypair), + self.gs_config, + self.subscription_filter, + self.data_transform, + ) + .unwrap(); + + if let Some((scoring_params, scoring_thresholds)) = self.scoring { + gs.with_peer_score(scoring_params, scoring_thresholds) + .unwrap(); + } + + let mut topic_hashes = vec![]; + + // subscribe to the topics + for t in self.topics { + let topic = Topic::new(t); + gs.subscribe(&topic).unwrap(); + topic_hashes.push(topic.hash().clone()); + } + + // build and connect peer_no random peers + let mut peers = vec![]; + let mut queues = HashMap::new(); + + let empty = vec![]; + for i in 0..self.peer_no { + let (peer, queue) = add_peer_with_addr_and_kind( + &mut gs, + if self.to_subscribe { + &topic_hashes + } else { + &empty + }, + i < self.outbound, + i < self.explicit, + Multiaddr::empty(), + self.peer_kind.or(Some(PeerKind::Gossipsubv1_1)), + ); + peers.push(peer); + queues.insert(peer, queue); + } + + (gs, peers, queues, topic_hashes) + } + + /// Sets the number of peers to create in the test network. + pub(super) fn peer_no(mut self, peer_no: usize) -> Self { + self.peer_no = peer_no; + self + } + + /// Sets the topics that the local node will subscribe to. + pub(super) fn topics(mut self, topics: Vec) -> Self { + self.topics = topics; + self + } + + /// If `true`, peers will also subscribe to the configured topics. + /// If `false`, peers are connected but not subscribed. + #[allow(clippy::wrong_self_convention)] + pub(super) fn to_subscribe(mut self, to_subscribe: bool) -> Self { + self.to_subscribe = to_subscribe; + self + } + + /// Sets a custom gossipsub configuration. + pub(super) fn gs_config(mut self, gs_config: Config) -> Self { + self.gs_config = gs_config; + self + } + + /// Sets how many of the first N peers should be marked as explicit peers. + pub(super) fn explicit(mut self, explicit: usize) -> Self { + self.explicit = explicit; + self + } + + /// Sets how many of the first N peers should be outbound connections. + /// Outbound peers are dialed by us; inbound peers connected to us. + pub(super) fn outbound(mut self, outbound: usize) -> Self { + self.outbound = outbound; + self + } + + /// Enables peer scoring with the given parameters and thresholds. + pub(super) fn scoring( + mut self, + scoring: Option<(PeerScoreParams, PeerScoreThresholds)>, + ) -> Self { + self.scoring = scoring; + self + } + + /// Sets a custom subscription filter. + pub(super) fn subscription_filter(mut self, subscription_filter: F) -> Self { + self.subscription_filter = subscription_filter; + self + } + + /// Sets the protocol version for all created peers (e.g., Gossipsubv1_1, Gossipsubv1_2). + pub(super) fn peer_kind(mut self, peer_kind: PeerKind) -> Self { + self.peer_kind = Some(peer_kind); + self + } +} + +/// Adds a new peer to the gossipsub network with default settings. +/// +/// This is a convenience wrapper around [`add_peer_with_addr_and_kind`] that uses +/// an empty multiaddr and Gossipsubv1_1 protocol. +/// +/// # Arguments +/// +/// * `gs` - The gossipsub behaviour to add the peer to +/// * `topic_hashes` - Topics the peer will be subscribed to +/// * `outbound` - If `true`, simulates an outbound connection (we dialed them) +/// * `explicit` - If `true`, adds the peer as an explicit peer +/// +/// # Returns +/// +/// A tuple of the peer's ID and their message queue (for inspecting sent messages). +pub(super) fn add_peer( + gs: &mut Behaviour, + topic_hashes: &[TopicHash], + outbound: bool, + explicit: bool, +) -> (PeerId, Queue) +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + add_peer_with_addr(gs, topic_hashes, outbound, explicit, Multiaddr::empty()) +} + +/// Adds a new peer with a specific multiaddr. +/// +/// See [`add_peer`] for basic usage. Use this variant when testing +/// address-specific behaviour (e.g., IP colocation scoring). +pub(super) fn add_peer_with_addr( + gs: &mut Behaviour, + topic_hashes: &[TopicHash], + outbound: bool, + explicit: bool, + address: Multiaddr, +) -> (PeerId, Queue) +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + add_peer_with_addr_and_kind( + gs, + topic_hashes, + outbound, + explicit, + address, + Some(PeerKind::Gossipsubv1_1), + ) +} + +/// Adds a new peer with full configuration options. +/// +/// This is the most flexible peer creation function, allowing control over +/// the peer's address and protocol version. +/// +/// # Arguments +/// +/// * `gs` - The gossipsub behaviour to add the peer to +/// * `topic_hashes` - Topics the peer will be subscribed to +/// * `outbound` - If `true`, simulates an outbound connection (we dialed them) +/// * `explicit` - If `true`, adds the peer as an explicit peer +/// * `address` - The multiaddr for the peer connection +/// * `kind` - The gossipsub protocol version. Use `None` for Floodsub peers, or +/// `Some(PeerKind::...)` for specific versions. +/// +/// # Returns +/// +/// A tuple of the peer's ID and their message queue (for inspecting sent messages). +pub(super) fn add_peer_with_addr_and_kind( + gs: &mut Behaviour, + topic_hashes: &[TopicHash], + outbound: bool, + explicit: bool, + address: Multiaddr, + kind: Option, +) -> (PeerId, Queue) +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + let peer = PeerId::random(); + let endpoint = if outbound { + ConnectedPoint::Dialer { + address, + role_override: Endpoint::Dialer, + port_use: PortUse::Reuse, + } + } else { + ConnectedPoint::Listener { + local_addr: Multiaddr::empty(), + send_back_addr: address, + } + }; + + let queue = Queue::new(gs.config.connection_handler_queue_len()); + let receiver_queue = queue.clone(); + let connection_id = ConnectionId::new_unchecked(0); + gs.connected_peers.insert( + peer, + PeerDetails { + kind: kind.unwrap_or(PeerKind::Floodsub), + outbound, + connections: vec![connection_id], + topics: Default::default(), + messages: queue, + dont_send: LinkedHashMap::new(), + }, + ); + + gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id: peer, + connection_id, + endpoint: &endpoint, + failed_addresses: &[], + other_established: 0, // first connection + })); + if let Some(kind) = kind { + gs.on_connection_handler_event( + peer, + ConnectionId::new_unchecked(0), + HandlerEvent::PeerKind(kind), + ); + } + if explicit { + gs.add_explicit_peer(&peer); + } + if !topic_hashes.is_empty() { + gs.handle_received_subscriptions( + &topic_hashes + .iter() + .cloned() + .map(|t| Subscription { + action: SubscriptionAction::Subscribe, + topic_hash: t, + }) + .collect::>(), + &peer, + ); + } + (peer, receiver_queue) +} + +/// Simulates disconnecting a peer from the gossipsub network. +/// +/// This triggers the `ConnectionClosed` event for all of the peer's connections, +/// properly cleaning up mesh membership and other peer state. +pub(super) fn disconnect_peer(gs: &mut Behaviour, peer_id: &PeerId) +where + D: DataTransform + Default + Clone + Send + 'static, + F: TopicSubscriptionFilter + Clone + Default + Send + 'static, +{ + if let Some(peer_connections) = gs.connected_peers.get(peer_id) { + let fake_endpoint = ConnectedPoint::Dialer { + address: Multiaddr::empty(), + role_override: Endpoint::Dialer, + port_use: PortUse::Reuse, + }; // this is not relevant + // peer_connections.connections should never be empty. + + let mut active_connections = peer_connections.connections.len(); + for connection_id in peer_connections.connections.clone() { + active_connections = active_connections.checked_sub(1).unwrap(); + + gs.on_swarm_event(FromSwarm::ConnectionClosed(ConnectionClosed { + peer_id: *peer_id, + connection_id, + endpoint: &fake_endpoint, + remaining_established: active_connections, + cause: None, + })); + } + } +} + +/// Converts a raw protobuf RPC message into a gossipsub [`RpcIn`] structure. +/// +/// This is useful for simulating incoming RPC messages from peers in tests. +/// It parses all message types: publish messages, subscriptions, and control +/// messages (IHAVE, IWANT, GRAFT, PRUNE). +pub(super) fn proto_to_message(rpc: &proto::RPC) -> RpcIn { + // Store valid messages. + let mut messages = Vec::with_capacity(rpc.publish.len()); + let rpc = rpc.clone(); + for message in rpc.publish.into_iter() { + messages.push(RawMessage { + source: message.from.map(|x| PeerId::from_bytes(&x).unwrap()), + data: message.data.unwrap_or_default(), + sequence_number: message.seqno.map(|x| BigEndian::read_u64(&x)), + topic: TopicHash::from_raw(message.topic), + signature: message.signature, + key: None, + validated: false, + }); + } + let mut control_msgs = Vec::new(); + if let Some(rpc_control) = rpc.control { + // Collect the gossipsub control messages + let ihave_msgs: Vec = rpc_control + .ihave + .into_iter() + .map(|ihave| { + ControlAction::IHave(IHave { + topic_hash: TopicHash::from_raw(ihave.topic_id.unwrap_or_default()), + message_ids: ihave + .message_ids + .into_iter() + .map(MessageId::from) + .collect::>(), + }) + }) + .collect(); + + let iwant_msgs: Vec = rpc_control + .iwant + .into_iter() + .map(|iwant| { + ControlAction::IWant(IWant { + message_ids: iwant + .message_ids + .into_iter() + .map(MessageId::from) + .collect::>(), + }) + }) + .collect(); + + let graft_msgs: Vec = rpc_control + .graft + .into_iter() + .map(|graft| { + ControlAction::Graft(Graft { + topic_hash: TopicHash::from_raw(graft.topic_id.unwrap_or_default()), + }) + }) + .collect(); + + let mut prune_msgs = Vec::new(); + + for prune in rpc_control.prune { + // filter out invalid peers + let peers = prune + .peers + .into_iter() + .filter_map(|info| { + info.peer_id + .and_then(|id| PeerId::from_bytes(&id).ok()) + .map(|peer_id| + //TODO signedPeerRecord, see https://github.com/libp2p/specs/pull/217 + PeerInfo { + peer_id: Some(peer_id), + }) + }) + .collect::>(); + + let topic_hash = TopicHash::from_raw(prune.topic_id.unwrap_or_default()); + prune_msgs.push(ControlAction::Prune(Prune { + topic_hash, + peers, + backoff: prune.backoff, + })); + } + + control_msgs.extend(ihave_msgs); + control_msgs.extend(iwant_msgs); + control_msgs.extend(graft_msgs); + control_msgs.extend(prune_msgs); + } + + RpcIn { + messages, + subscriptions: rpc + .subscriptions + .into_iter() + .map(|sub| Subscription { + action: if Some(true) == sub.subscribe { + SubscriptionAction::Subscribe + } else { + SubscriptionAction::Unsubscribe + }, + topic_hash: TopicHash::from_raw(sub.topic_id.unwrap_or_default()), + }) + .collect(), + control_msgs, + } +} + +impl Behaviour { + /// Returns a mutable reference to the peer scoring state. + /// + /// # Panics + /// + /// Panics if peer scoring is not enabled on this behaviour instance. + /// Ensure scoring is configured via [`BehaviourTestBuilder::scoring`] before calling. + pub(super) fn as_peer_score_mut(&mut self) -> &mut PeerScore { + match self.peer_score { + PeerScoreState::Active(ref mut peer_score) => peer_score, + PeerScoreState::Disabled => panic!("PeerScore is deactivated"), + } + } +} + +/// Counts RPC messages across all peer queues that match a filter predicate. +/// +/// This drains all messages from the queues, counting those that match the filter. +/// Returns the count and the (now empty) queues for potential reuse. +/// +/// # Arguments +/// +/// * `queues` - Map of peer IDs to their message queues +/// * `filter` - Predicate that receives (peer_id, rpc_message) and returns true to count +/// +/// # Returns +/// +/// A tuple of (count of matching messages, emptied queues map). +/// +/// # Example +/// +/// ```ignore +/// let (prune_count, queues) = count_control_msgs(queues, |peer_id, msg| { +/// matches!(msg, RpcOut::Prune { .. }) +/// }); +/// ``` +pub(super) fn count_control_msgs( + queues: HashMap, + mut filter: impl FnMut(&PeerId, &RpcOut) -> bool, +) -> (usize, HashMap) { + let mut new_queues = HashMap::new(); + let mut collected_messages = 0; + for (peer_id, mut queue) in queues.into_iter() { + while !queue.is_empty() { + if let Some(rpc) = queue.try_pop() { + if filter(&peer_id, &rpc) { + collected_messages += 1; + } + } + } + new_queues.insert(peer_id, queue); + } + (collected_messages, new_queues) +} + +/// Clears all pending events from the behaviour and drains all peer message queues. +/// +/// Use this to reset state between test phases when you want to ignore +/// messages/events generated by setup operations. +/// +/// # Returns +/// +/// The emptied queues map for continued use. +pub(super) fn flush_events( + gs: &mut Behaviour, + queues: HashMap, +) -> HashMap { + gs.events.clear(); + let mut new_queues = HashMap::new(); + for (peer_id, mut queue) in queues.into_iter() { + while !queue.is_empty() { + let _ = queue.try_pop(); + } + new_queues.insert(peer_id, queue); + } + new_queues +} + +/// Generates a random gossipsub message for testing. +/// +/// Creates a message with: +/// - Random source peer ID +/// - Random data (10-10024 bytes) +/// - Sequential sequence number (using the provided counter) +/// - Random topic from the provided list +/// - Pre-validated status +/// +/// # Arguments +/// +/// * `seq` - Mutable sequence counter (incremented each call) +/// * `topics` - Pool of topics to randomly select from +pub(super) fn random_message(seq: &mut u64, topics: &[TopicHash]) -> RawMessage { + let mut rng = rand::thread_rng(); + *seq += 1; + RawMessage { + source: Some(PeerId::random()), + data: (0..rng.gen_range(10..10024)).map(|_| rng.gen()).collect(), + sequence_number: Some(*seq), + topic: topics[rng.gen_range(0..topics.len())].clone(), + signature: None, + key: None, + validated: true, + } +} diff --git a/protocols/gossipsub/src/behaviour/tests/peer_queues.rs b/protocols/gossipsub/src/behaviour/tests/peer_queues.rs new file mode 100644 index 00000000000..f70ae66d257 --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/peer_queues.rs @@ -0,0 +1,472 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Tests for the peer queues, namely back pressure and penalisation of slow peers. + +use std::collections::{BTreeSet, HashSet}; + +use hashlink::LinkedHashMap; +use libp2p_identity::PeerId; +use libp2p_swarm::{ConnectionId, ToSwarm}; + +use crate::{ + config::ConfigBuilder, + peer_score::{PeerScoreParams, PeerScoreThresholds}, + queue::Queue, + transform::DataTransform, + types::{Message, PeerDetails, PeerKind}, + Behaviour, Event, FailedMessages, IdentTopic as Topic, MessageAuthenticity, PublishError, + ValidationMode, +}; + +#[test] +fn test_all_queues_full() { + let gs_config = ConfigBuilder::default() + .validation_mode(ValidationMode::Permissive) + .build() + .unwrap(); + + let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); + + let topic_hash = Topic::new("Test").hash(); + let mut peers = vec![]; + let mut topics = BTreeSet::new(); + topics.insert(topic_hash.clone()); + + let peer_id = PeerId::random(); + peers.push(peer_id); + gs.connected_peers.insert( + peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(1), + dont_send: LinkedHashMap::new(), + }, + ); + + let publish_data = vec![2; 59]; + let result = gs.publish(topic_hash.clone(), publish_data.clone()); + assert!(result.is_ok()); + let err = gs.publish(topic_hash, publish_data).unwrap_err(); + assert!(matches!(err, PublishError::AllQueuesFull(f) if f == 1)); +} + +#[test] +fn test_slow_peer_returns_failed_publish() { + let gs_config = ConfigBuilder::default() + .validation_mode(ValidationMode::Permissive) + .build() + .unwrap(); + + let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); + + let topic_hash = Topic::new("Test").hash(); + let mut peers = vec![]; + let mut topics = BTreeSet::new(); + topics.insert(topic_hash.clone()); + + let slow_peer_id = PeerId::random(); + peers.push(slow_peer_id); + let mesh = gs.mesh.entry(topic_hash.clone()).or_default(); + mesh.insert(slow_peer_id); + gs.connected_peers.insert( + slow_peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(1), + dont_send: LinkedHashMap::new(), + }, + ); + let peer_id = PeerId::random(); + peers.push(peer_id); + gs.connected_peers.insert( + peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(gs.config.connection_handler_queue_len()), + dont_send: LinkedHashMap::new(), + }, + ); + + let publish_data = vec![0; 42]; + let _failed_publish = gs.publish(topic_hash.clone(), publish_data.clone()); + let _failed_publish = gs.publish(topic_hash.clone(), publish_data.clone()); + gs.heartbeat(); + + let slow_peer_failed_messages = gs + .events + .into_iter() + .find_map(|e| match e { + ToSwarm::GenerateEvent(Event::SlowPeer { + peer_id, + failed_messages, + }) if peer_id == slow_peer_id => Some(failed_messages), + _ => None, + }) + .expect("No SlowPeer event found"); + + let failed_messages = FailedMessages { + priority: 0, + non_priority: 1, + }; + + assert_eq!(slow_peer_failed_messages, failed_messages); +} + +#[test] +fn test_slow_peer_returns_failed_ihave_handling() { + let gs_config = ConfigBuilder::default() + .validation_mode(ValidationMode::Permissive) + .build() + .unwrap(); + + let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); + + let topic_hash = Topic::new("Test").hash(); + let mut peers = vec![]; + let mut topics = BTreeSet::new(); + topics.insert(topic_hash.clone()); + + let slow_peer_id = PeerId::random(); + gs.connected_peers.insert( + slow_peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(1), + dont_send: LinkedHashMap::new(), + }, + ); + peers.push(slow_peer_id); + let mesh = gs.mesh.entry(topic_hash.clone()).or_default(); + mesh.insert(slow_peer_id); + + let peer_id = PeerId::random(); + peers.push(peer_id); + gs.connected_peers.insert( + peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(gs.config.connection_handler_queue_len()), + dont_send: LinkedHashMap::new(), + }, + ); + + // First message. + let publish_data = vec![1; 59]; + let transformed = gs + .data_transform + .outbound_transform(&topic_hash, publish_data.clone()) + .unwrap(); + let raw_message = gs + .build_raw_message(topic_hash.clone(), transformed) + .unwrap(); + let msg_id = gs.config.message_id(&Message { + source: raw_message.source, + data: publish_data, + sequence_number: raw_message.sequence_number, + topic: raw_message.topic.clone(), + }); + + gs.handle_ihave( + &slow_peer_id, + vec![(topic_hash.clone(), vec![msg_id.clone()])], + ); + + // Second message. + let publish_data = vec![2; 59]; + let transformed = gs + .data_transform + .outbound_transform(&topic_hash, publish_data.clone()) + .unwrap(); + let raw_message = gs + .build_raw_message(topic_hash.clone(), transformed) + .unwrap(); + let msg_id = gs.config.message_id(&Message { + source: raw_message.source, + data: publish_data, + sequence_number: raw_message.sequence_number, + topic: raw_message.topic.clone(), + }); + gs.handle_ihave(&slow_peer_id, vec![(topic_hash, vec![msg_id.clone()])]); + + gs.heartbeat(); + + let slow_peer_failed_messages = gs + .events + .into_iter() + .find_map(|e| match e { + ToSwarm::GenerateEvent(Event::SlowPeer { + peer_id, + failed_messages, + }) if peer_id == slow_peer_id => Some(failed_messages), + _ => None, + }) + .unwrap(); + + let failed_messages = FailedMessages { + priority: 0, + non_priority: 1, + }; + + assert_eq!(slow_peer_failed_messages, failed_messages); +} + +#[test] +fn test_slow_peer_returns_failed_iwant_handling() { + let gs_config = ConfigBuilder::default() + .validation_mode(ValidationMode::Permissive) + .build() + .unwrap(); + + let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); + + let topic_hash = Topic::new("Test").hash(); + let mut peers = vec![]; + let mut topics = BTreeSet::new(); + topics.insert(topic_hash.clone()); + + let slow_peer_id = PeerId::random(); + peers.push(slow_peer_id); + gs.connected_peers.insert( + slow_peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(1), + dont_send: LinkedHashMap::new(), + }, + ); + peers.push(slow_peer_id); + let mesh = gs.mesh.entry(topic_hash.clone()).or_default(); + mesh.insert(slow_peer_id); + + let peer_id = PeerId::random(); + peers.push(peer_id); + gs.connected_peers.insert( + peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(gs.config.connection_handler_queue_len()), + dont_send: LinkedHashMap::new(), + }, + ); + + let publish_data = vec![1; 59]; + let transformed = gs + .data_transform + .outbound_transform(&topic_hash, publish_data.clone()) + .unwrap(); + let raw_message = gs + .build_raw_message(topic_hash.clone(), transformed) + .unwrap(); + let msg_id = gs.config.message_id(&Message { + source: raw_message.source, + data: publish_data, + sequence_number: raw_message.sequence_number, + topic: raw_message.topic.clone(), + }); + + gs.mcache.put(&msg_id, raw_message); + gs.handle_iwant(&slow_peer_id, vec![msg_id.clone(), msg_id]); + + gs.heartbeat(); + + let slow_peer_failed_messages = gs + .events + .into_iter() + .find_map(|e| match e { + ToSwarm::GenerateEvent(Event::SlowPeer { + peer_id, + failed_messages, + }) if peer_id == slow_peer_id => Some(failed_messages), + _ => None, + }) + .unwrap(); + + let failed_messages = FailedMessages { + priority: 0, + non_priority: 1, + }; + + assert_eq!(slow_peer_failed_messages, failed_messages); +} + +#[test] +fn test_slow_peer_returns_failed_forward() { + let gs_config = ConfigBuilder::default() + .validation_mode(ValidationMode::Permissive) + .build() + .unwrap(); + + let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); + + let topic_hash = Topic::new("Test").hash(); + let mut peers = vec![]; + let mut topics = BTreeSet::new(); + topics.insert(topic_hash.clone()); + + let slow_peer_id = PeerId::random(); + peers.push(slow_peer_id); + gs.connected_peers.insert( + slow_peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(1), + dont_send: LinkedHashMap::new(), + }, + ); + peers.push(slow_peer_id); + let mesh = gs.mesh.entry(topic_hash.clone()).or_default(); + mesh.insert(slow_peer_id); + + let peer_id = PeerId::random(); + peers.push(peer_id); + gs.connected_peers.insert( + peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(gs.config.connection_handler_queue_len()), + dont_send: LinkedHashMap::new(), + }, + ); + + let publish_data = vec![1; 59]; + let transformed = gs + .data_transform + .outbound_transform(&topic_hash, publish_data.clone()) + .unwrap(); + let raw_message = gs + .build_raw_message(topic_hash.clone(), transformed) + .unwrap(); + let msg_id = gs.config.message_id(&Message { + source: raw_message.source, + data: publish_data, + sequence_number: raw_message.sequence_number, + topic: raw_message.topic.clone(), + }); + + gs.forward_msg(&msg_id, raw_message.clone(), None, HashSet::new()); + gs.forward_msg(&msg_id, raw_message, None, HashSet::new()); + + gs.heartbeat(); + + let slow_peer_failed_messages = gs + .events + .into_iter() + .find_map(|e| match e { + ToSwarm::GenerateEvent(Event::SlowPeer { + peer_id, + failed_messages, + }) if peer_id == slow_peer_id => Some(failed_messages), + _ => None, + }) + .unwrap(); + + let failed_messages = FailedMessages { + non_priority: 1, + priority: 0, + }; + + assert_eq!(slow_peer_failed_messages, failed_messages); +} + +#[test] +fn test_slow_peer_is_downscored_on_publish() { + let gs_config = ConfigBuilder::default() + .validation_mode(ValidationMode::Permissive) + .build() + .unwrap(); + + let mut gs: Behaviour = Behaviour::new(MessageAuthenticity::RandomAuthor, gs_config).unwrap(); + let slow_peer_params = PeerScoreParams::default(); + gs.with_peer_score(slow_peer_params.clone(), PeerScoreThresholds::default()) + .unwrap(); + + let topic_hash = Topic::new("Test").hash(); + let mut peers = vec![]; + let mut topics = BTreeSet::new(); + topics.insert(topic_hash.clone()); + + let slow_peer_id = PeerId::random(); + peers.push(slow_peer_id); + let mesh = gs.mesh.entry(topic_hash.clone()).or_default(); + mesh.insert(slow_peer_id); + gs.connected_peers.insert( + slow_peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(1), + dont_send: LinkedHashMap::new(), + }, + ); + gs.as_peer_score_mut().add_peer(slow_peer_id); + let peer_id = PeerId::random(); + peers.push(peer_id); + gs.connected_peers.insert( + peer_id, + PeerDetails { + kind: PeerKind::Gossipsubv1_1, + connections: vec![ConnectionId::new_unchecked(0)], + outbound: false, + topics: topics.clone(), + messages: Queue::new(gs.config.connection_handler_queue_len()), + dont_send: LinkedHashMap::new(), + }, + ); + + let publish_data = vec![0; 42]; + gs.publish(topic_hash.clone(), publish_data.clone()) + .unwrap(); + let publish_data = vec![2; 59]; + gs.publish(topic_hash.clone(), publish_data).unwrap(); + gs.heartbeat(); + let slow_peer_score = gs.peer_score(&slow_peer_id).unwrap(); + // There should be two penalties for the two failed messages. + assert_eq!(slow_peer_score, slow_peer_params.slow_peer_weight * 2.0); +} diff --git a/protocols/gossipsub/src/behaviour/tests/publish.rs b/protocols/gossipsub/src/behaviour/tests/publish.rs new file mode 100644 index 00000000000..181fb60f24c --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/publish.rs @@ -0,0 +1,245 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Tests for publishing and fanout functionality. + +use super::DefaultBehaviourTestBuilder; +use crate::{ + config::{Config, ConfigBuilder}, + topic::TopicHash, + transform::DataTransform, + types::RpcOut, + IdentTopic as Topic, +}; + +/// Test local node publish to subscribed topic +#[test] +fn test_publish_without_flood_publishing() { + // node should: + // - Send publish message to all peers + // - Insert message into gs.mcache and gs.received + + // turn off flood publish to test old behaviour + let config = ConfigBuilder::default() + .flood_publish(false) + .build() + .unwrap(); + + let publish_topic = String::from("test_publish"); + let (mut gs, _, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(vec![publish_topic.clone()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + assert!( + gs.mesh.contains_key(&topic_hashes[0]), + "Subscribe should add a new entry to the mesh[topic] hashmap" + ); + + // all peers should be subscribed to the topic + assert_eq!( + gs.connected_peers + .values() + .filter(|p| p.topics.contains(&topic_hashes[0])) + .count(), + 20, + "Peers should be subscribed to the topic" + ); + + // publish on topic + let publish_data = vec![0; 42]; + gs.publish(Topic::new(publish_topic), publish_data).unwrap(); + + // Collect all publish messages + let publishes = queues + .into_values() + .fold(vec![], |mut collected_publish, mut queue| { + while !queue.is_empty() { + if let Some(RpcOut::Publish { message, .. }) = queue.try_pop() { + collected_publish.push(message); + } + } + collected_publish + }); + + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform( + publishes + .first() + .expect("Should contain > 0 entries") + .clone(), + ) + .unwrap(); + + let msg_id = gs.config.message_id(message); + + let config: Config = Config::default(); + assert_eq!( + publishes.len(), + config.mesh_n(), + "Should send a publish message to at least mesh_n peers" + ); + + assert!( + gs.mcache.get(&msg_id).is_some(), + "Message cache should contain published message" + ); +} + +/// Test local node publish to unsubscribed topic +#[test] +fn test_fanout() { + // node should: + // - Populate fanout peers + // - Send publish message to fanout peers + // - Insert message into gs.mcache and gs.received + + // turn off flood publish to test fanout behaviour + let config = ConfigBuilder::default() + .flood_publish(false) + .build() + .unwrap(); + + let fanout_topic = String::from("test_fanout"); + let (mut gs, _, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(vec![fanout_topic.clone()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + assert!( + gs.mesh.contains_key(&topic_hashes[0]), + "Subscribe should add a new entry to the mesh[topic] hashmap" + ); + // Unsubscribe from topic + assert!( + gs.unsubscribe(&Topic::new(fanout_topic.clone())), + "should be able to unsubscribe successfully from topic" + ); + + // Publish on unsubscribed topic + let publish_data = vec![0; 42]; + gs.publish(Topic::new(fanout_topic.clone()), publish_data) + .unwrap(); + + assert_eq!( + gs.fanout + .get(&TopicHash::from_raw(fanout_topic)) + .unwrap() + .len(), + gs.config.mesh_n(), + "Fanout should contain `mesh_n` peers for fanout topic" + ); + + // Collect all publish messages + let publishes = queues + .into_values() + .fold(vec![], |mut collected_publish, mut queue| { + while !queue.is_empty() { + if let Some(RpcOut::Publish { message, .. }) = queue.try_pop() { + collected_publish.push(message); + } + } + collected_publish + }); + + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform( + publishes + .first() + .expect("Should contain > 0 entries") + .clone(), + ) + .unwrap(); + + let msg_id = gs.config.message_id(message); + + assert_eq!( + publishes.len(), + gs.config.mesh_n(), + "Should send a publish message to `mesh_n` fanout peers" + ); + + assert!( + gs.mcache.get(&msg_id).is_some(), + "Message cache should contain published message" + ); +} + +#[test] +fn test_flood_publish() { + let config: Config = Config::default(); + + let topic = "test"; + // Adds more peers than mesh can hold to test flood publishing + let (mut gs, _, queues, _) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_high() + 10) + .topics(vec![topic.into()]) + .to_subscribe(true) + .create_network(); + + // publish message + let publish_data = vec![0; 42]; + gs.publish(Topic::new(topic), publish_data).unwrap(); + + // Collect all publish messages + let publishes = queues + .into_values() + .fold(vec![], |mut collected_publish, mut queue| { + while !queue.is_empty() { + if let Some(RpcOut::Publish { message, .. }) = queue.try_pop() { + collected_publish.push(message); + } + } + collected_publish + }); + + // Transform the inbound message + let message = &gs + .data_transform + .inbound_transform( + publishes + .first() + .expect("Should contain > 0 entries") + .clone(), + ) + .unwrap(); + + let msg_id = gs.config.message_id(message); + + let config: Config = Config::default(); + assert_eq!( + publishes.len(), + config.mesh_n_high() + 10, + "Should send a publish message to all known peers" + ); + + assert!( + gs.mcache.get(&msg_id).is_some(), + "Message cache should contain published message" + ); +} diff --git a/protocols/gossipsub/src/behaviour/tests/scoring.rs b/protocols/gossipsub/src/behaviour/tests/scoring.rs new file mode 100644 index 00000000000..977d7488956 --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/scoring.rs @@ -0,0 +1,2114 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Tests for peer scoring functionality. + +use std::{ + collections::{HashMap, HashSet}, + net::Ipv4Addr, + thread::sleep, + time::Duration, +}; + +use libp2p_core::{Endpoint, Multiaddr}; +use libp2p_identity::PeerId; +use libp2p_swarm::{ConnectionId, FromSwarm, NetworkBehaviour, ToSwarm}; + +use super::{ + add_peer, add_peer_with_addr, count_control_msgs, proto_to_message, random_message, + DefaultBehaviourTestBuilder, +}; +use crate::{ + behaviour::{ConnectionEstablished, PortUse}, + config::{Config, ConfigBuilder}, + error::ValidationError, + handler::HandlerEvent, + peer_score::{PeerScoreParams, PeerScoreThresholds, TopicScoreParams}, + queue::Queue, + transform::DataTransform, + types::{ + ControlAction, IHave, IWant, MessageAcceptance, PeerInfo, Prune, RawMessage, RpcIn, RpcOut, + Subscription, SubscriptionAction, + }, + IdentTopic as Topic, +}; + +#[test] +fn test_prune_negative_scored_peers() { + let config = Config::default(); + + // build mesh with one peer + let (mut gs, peers, queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + // add penalty to peer + gs.as_peer_score_mut().add_penalty(&peers[0], 1); + + // execute heartbeat + gs.heartbeat(); + + // peer should not be in mesh anymore + assert!(gs.mesh[&topics[0]].is_empty()); + + // check prune message + let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| { + peer_id == &peers[0] + && match m { + RpcOut::Prune(Prune { + topic_hash, + peers, + backoff, + }) => { + topic_hash == &topics[0] && + //no px in this case + peers.is_empty() && + backoff.unwrap() == config.prune_backoff().as_secs() + } + _ => false, + } + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_dont_graft_to_negative_scored_peers() { + let config = Config::default(); + // init full mesh + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + // add two additional peers that will not be part of the mesh + let (p1, _queue1) = add_peer(&mut gs, &topics, false, false); + let (p2, _queue2) = add_peer(&mut gs, &topics, false, false); + + // reduce score of p1 to negative + gs.as_peer_score_mut().add_penalty(&p1, 1); + + // handle prunes of all other peers + for p in peers { + gs.handle_prune(&p, vec![(topics[0].clone(), Vec::new(), None)]); + } + + // heartbeat + gs.heartbeat(); + + // assert that mesh only contains p2 + assert_eq!(gs.mesh.get(&topics[0]).unwrap().len(), 1); + assert!(gs.mesh.get(&topics[0]).unwrap().contains(&p2)); +} + +/// Note that in this test also without a penalty the px would be ignored because of the +/// acceptPXThreshold, but the spec still explicitly states the rule that px from negative +/// peers should get ignored, therefore we test it here. +#[test] +fn test_ignore_px_from_negative_scored_peer() { + let config = Config::default(); + + // build mesh with one peer + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + // penalize peer + gs.as_peer_score_mut().add_penalty(&peers[0], 1); + + // handle prune from single peer with px peers + let px = vec![PeerInfo { + peer_id: Some(PeerId::random()), + }]; + + gs.handle_prune( + &peers[0], + vec![( + topics[0].clone(), + px, + Some(config.prune_backoff().as_secs()), + )], + ); + + // assert no dials + assert_eq!( + gs.events + .iter() + .filter(|e| matches!(e, ToSwarm::Dial { .. })) + .count(), + 0 + ); +} + +#[test] +fn test_only_send_nonnegative_scoring_peers_in_px() { + let config = ConfigBuilder::default() + .prune_peers(16) + .do_px() + .build() + .unwrap(); + + // Build mesh with three peer + let (mut gs, peers, queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(3) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + // Penalize first peer + gs.as_peer_score_mut().add_penalty(&peers[0], 1); + + // Prune second peer + gs.send_graft_prune( + HashMap::new(), + vec![(peers[1], vec![topics[0].clone()])] + .into_iter() + .collect(), + HashSet::new(), + ); + + // Check that px in prune message only contains third peer + let (control_msgs, _) = count_control_msgs(queues, |peer_id, m| { + peer_id == &peers[1] + && match m { + RpcOut::Prune(Prune { + topic_hash, + peers: px, + .. + }) => { + topic_hash == &topics[0] + && px.len() == 1 + && px[0].peer_id.as_ref().unwrap() == &peers[2] + } + _ => false, + } + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_do_not_gossip_to_peers_below_gossip_threshold() { + let config = Config::default(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + + // Build full mesh + let (mut gs, peers, mut queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // Graft all the peer + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // Add two additional peers that will not be part of the mesh + let (p1, queue1) = add_peer(&mut gs, &topics, false, false); + queues.insert(p1, queue1); + let (p2, queue2) = add_peer(&mut gs, &topics, false, false); + queues.insert(p2, queue2); + + // Reduce score of p1 below peer_score_thresholds.gossip_threshold + // note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.as_peer_score_mut().add_penalty(&p1, 2); + + // Reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold + gs.as_peer_score_mut().add_penalty(&p2, 1); + + // Receive message + let raw_message = RawMessage { + source: Some(PeerId::random()), + data: vec![], + sequence_number: Some(0), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(raw_message.clone(), &PeerId::random()); + + // Transform the inbound message + let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); + + let msg_id = gs.config.message_id(message); + + // Emit gossip + gs.emit_gossip(); + + // Check that exactly one gossip messages got sent and it got sent to p2 + let (control_msgs, _) = count_control_msgs(queues, |peer, action| match action { + RpcOut::IHave(IHave { + topic_hash, + message_ids, + }) => { + if topic_hash == &topics[0] && message_ids.iter().any(|id| id == &msg_id) { + assert_eq!(peer, &p2); + true + } else { + false + } + } + _ => false, + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_iwant_msg_from_peer_below_gossip_threshold_gets_ignored() { + let config = Config::default(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + + // Build full mesh + let (mut gs, peers, mut queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // Graft all the peer + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // Add two additional peers that will not be part of the mesh + let (p1, queue1) = add_peer(&mut gs, &topics, false, false); + queues.insert(p1, queue1); + let (p2, queue2) = add_peer(&mut gs, &topics, false, false); + queues.insert(p2, queue2); + + // Reduce score of p1 below peer_score_thresholds.gossip_threshold + // note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.as_peer_score_mut().add_penalty(&p1, 2); + + // Reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold + gs.as_peer_score_mut().add_penalty(&p2, 1); + + // Receive message + let raw_message = RawMessage { + source: Some(PeerId::random()), + data: vec![], + sequence_number: Some(0), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + gs.handle_received_message(raw_message.clone(), &PeerId::random()); + + // Transform the inbound message + let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); + + let msg_id = gs.config.message_id(message); + + gs.handle_iwant(&p1, vec![msg_id.clone()]); + gs.handle_iwant(&p2, vec![msg_id.clone()]); + + // the messages we are sending + let sent_messages = + queues + .into_iter() + .fold(vec![], |mut collected_messages, (peer_id, mut queue)| { + while !queue.is_empty() { + if let Some(RpcOut::Forward { message, .. }) = queue.try_pop() { + collected_messages.push((peer_id, message)); + } + } + collected_messages + }); + + // the message got sent to p2 + assert!(sent_messages + .iter() + .map(|(peer_id, msg)| ( + peer_id, + gs.data_transform.inbound_transform(msg.clone()).unwrap() + )) + .any(|(peer_id, msg)| peer_id == &p2 && gs.config.message_id(&msg) == msg_id)); + // the message got not sent to p1 + assert!(sent_messages + .iter() + .map(|(peer_id, msg)| ( + peer_id, + gs.data_transform.inbound_transform(msg.clone()).unwrap() + )) + .all(|(peer_id, msg)| !(peer_id == &p1 && gs.config.message_id(&msg) == msg_id))); +} + +#[test] +fn test_ihave_msg_from_peer_below_gossip_threshold_gets_ignored() { + let config = Config::default(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + // build full mesh + let (mut gs, peers, mut queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_high()) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // graft all the peer + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + // add two additional peers that will not be part of the mesh + let (p1, queue1) = add_peer(&mut gs, &topics, false, false); + queues.insert(p1, queue1); + let (p2, queue2) = add_peer(&mut gs, &topics, false, false); + queues.insert(p2, queue2); + + // reduce score of p1 below peer_score_thresholds.gossip_threshold + // note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.as_peer_score_mut().add_penalty(&p1, 2); + + // reduce score of p2 below 0 but not below peer_score_thresholds.gossip_threshold + gs.as_peer_score_mut().add_penalty(&p2, 1); + + // message that other peers have + let raw_message = RawMessage { + source: Some(PeerId::random()), + data: vec![], + sequence_number: Some(0), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + + // Transform the inbound message + let message = &gs.data_transform.inbound_transform(raw_message).unwrap(); + + let msg_id = gs.config.message_id(message); + + gs.handle_ihave(&p1, vec![(topics[0].clone(), vec![msg_id.clone()])]); + gs.handle_ihave(&p2, vec![(topics[0].clone(), vec![msg_id.clone()])]); + + // check that we sent exactly one IWANT request to p2 + let (control_msgs, _) = count_control_msgs(queues, |peer, c| match c { + RpcOut::IWant(IWant { message_ids }) => { + if message_ids.iter().any(|m| m == &msg_id) { + assert_eq!(peer, &p2); + true + } else { + false + } + } + _ => false, + }); + assert_eq!(control_msgs, 1); +} + +#[test] +fn test_do_not_publish_to_peer_below_publish_threshold() { + let config = ConfigBuilder::default() + .flood_publish(false) + .build() + .unwrap(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, + publish_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + + // build mesh with no peers and no subscribed topics + let (mut gs, _, mut queues, _) = DefaultBehaviourTestBuilder::default() + .gs_config(config) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // create a new topic for which we are not subscribed + let topic = Topic::new("test"); + let topics = vec![topic.hash()]; + + // add two additional peers that will be added to the mesh + let (p1, queue1) = add_peer(&mut gs, &topics, false, false); + queues.insert(p1, queue1); + let (p2, queue2) = add_peer(&mut gs, &topics, false, false); + queues.insert(p2, queue2); + + // reduce score of p1 below peer_score_thresholds.publish_threshold + // note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.as_peer_score_mut().add_penalty(&p1, 2); + + // reduce score of p2 below 0 but not below peer_score_thresholds.publish_threshold + gs.as_peer_score_mut().add_penalty(&p2, 1); + + // a heartbeat will remove the peers from the mesh + gs.heartbeat(); + + // publish on topic + let publish_data = vec![0; 42]; + gs.publish(topic, publish_data).unwrap(); + + // Collect all publish messages + let publishes = + queues + .into_iter() + .fold(vec![], |mut collected_publish, (peer_id, mut queue)| { + while !queue.is_empty() { + if let Some(RpcOut::Publish { message, .. }) = queue.try_pop() { + collected_publish.push((peer_id, message)); + } + } + collected_publish + }); + + // assert only published to p2 + assert_eq!(publishes.len(), 1); + assert_eq!(publishes[0].0, p2); +} + +#[test] +fn test_do_not_flood_publish_to_peer_below_publish_threshold() { + let config = Config::default(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, + publish_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + // build mesh with no peers + let (mut gs, _, mut queues, topics) = DefaultBehaviourTestBuilder::default() + .topics(vec!["test".into()]) + .gs_config(config) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // add two additional peers that will be added to the mesh + let (p1, queue1) = add_peer(&mut gs, &topics, false, false); + queues.insert(p1, queue1); + let (p2, queue2) = add_peer(&mut gs, &topics, false, false); + queues.insert(p2, queue2); + + // reduce score of p1 below peer_score_thresholds.publish_threshold + // note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.as_peer_score_mut().add_penalty(&p1, 2); + + // reduce score of p2 below 0 but not below peer_score_thresholds.publish_threshold + gs.as_peer_score_mut().add_penalty(&p2, 1); + + // a heartbeat will remove the peers from the mesh + gs.heartbeat(); + + // publish on topic + let publish_data = vec![0; 42]; + gs.publish(Topic::new("test"), publish_data).unwrap(); + + // Collect all publish messages + let publishes = + queues + .into_iter() + .fold(vec![], |mut collected_publish, (peer_id, mut queue)| { + while !queue.is_empty() { + if let Some(RpcOut::Publish { message, .. }) = queue.try_pop() { + collected_publish.push((peer_id, message)) + } + } + collected_publish + }); + + // assert only published to p2 + assert_eq!(publishes.len(), 1); + assert!(publishes[0].0 == p2); +} + +#[test] +fn test_ignore_rpc_from_peers_below_graylist_threshold() { + let config = Config::default(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + gossip_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, + publish_threshold: 0.5 * peer_score_params.behaviour_penalty_weight, + graylist_threshold: 3.0 * peer_score_params.behaviour_penalty_weight, + ..PeerScoreThresholds::default() + }; + + // build mesh with no peers + let (mut gs, _, _, topics) = DefaultBehaviourTestBuilder::default() + .topics(vec!["test".into()]) + .gs_config(config.clone()) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // add two additional peers that will be added to the mesh + let (p1, _queue1) = add_peer(&mut gs, &topics, false, false); + let (p2, _queue2) = add_peer(&mut gs, &topics, false, false); + + // reduce score of p1 below peer_score_thresholds.graylist_threshold + // note that penalties get squared so two penalties means a score of + // 4 * peer_score_params.behaviour_penalty_weight. + gs.as_peer_score_mut().add_penalty(&p1, 2); + + // reduce score of p2 below publish_threshold but not below graylist_threshold + gs.as_peer_score_mut().add_penalty(&p2, 1); + + let raw_message1 = RawMessage { + source: Some(PeerId::random()), + data: vec![1, 2, 3, 4], + sequence_number: Some(1u64), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + + let raw_message2 = RawMessage { + source: Some(PeerId::random()), + data: vec![1, 2, 3, 4, 5], + sequence_number: Some(2u64), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + + let raw_message3 = RawMessage { + source: Some(PeerId::random()), + data: vec![1, 2, 3, 4, 5, 6], + sequence_number: Some(3u64), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + + let raw_message4 = RawMessage { + source: Some(PeerId::random()), + data: vec![1, 2, 3, 4, 5, 6, 7], + sequence_number: Some(4u64), + topic: topics[0].clone(), + signature: None, + key: None, + validated: true, + }; + + // Transform the inbound message + let message2 = &gs.data_transform.inbound_transform(raw_message2).unwrap(); + + // Transform the inbound message + let message4 = &gs.data_transform.inbound_transform(raw_message4).unwrap(); + + let subscription = Subscription { + action: SubscriptionAction::Subscribe, + topic_hash: topics[0].clone(), + }; + + let control_action = ControlAction::IHave(IHave { + topic_hash: topics[0].clone(), + message_ids: vec![config.message_id(message2)], + }); + + // clear events + gs.events.clear(); + + // receive from p1 + gs.on_connection_handler_event( + p1, + ConnectionId::new_unchecked(0), + HandlerEvent::Message { + rpc: RpcIn { + messages: vec![raw_message1], + subscriptions: vec![subscription.clone()], + control_msgs: vec![control_action], + }, + invalid_messages: Vec::new(), + }, + ); + + // only the subscription event gets processed, the rest is dropped + assert_eq!(gs.events.len(), 1); + assert!(matches!( + gs.events[0], + ToSwarm::GenerateEvent(crate::Event::Subscribed { .. }) + )); + + let control_action = ControlAction::IHave(IHave { + topic_hash: topics[0].clone(), + message_ids: vec![config.message_id(message4)], + }); + + // receive from p2 + gs.on_connection_handler_event( + p2, + ConnectionId::new_unchecked(0), + HandlerEvent::Message { + rpc: RpcIn { + messages: vec![raw_message3], + subscriptions: vec![subscription], + control_msgs: vec![control_action], + }, + invalid_messages: Vec::new(), + }, + ); + + // events got processed + assert!(gs.events.len() > 1); +} + +#[test] +fn test_ignore_px_from_peers_below_accept_px_threshold() { + let config = ConfigBuilder::default().prune_peers(16).build().unwrap(); + let peer_score_params = PeerScoreParams::default(); + let peer_score_thresholds = PeerScoreThresholds { + accept_px_threshold: peer_score_params.app_specific_weight, + ..PeerScoreThresholds::default() + }; + // Build mesh with two peers + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // Decrease score of first peer to less than accept_px_threshold + gs.set_application_score(&peers[0], 0.99); + + // Increase score of second peer to accept_px_threshold + gs.set_application_score(&peers[1], 1.0); + + // Handle prune from peer peers[0] with px peers + let px = vec![PeerInfo { + peer_id: Some(PeerId::random()), + }]; + gs.handle_prune( + &peers[0], + vec![( + topics[0].clone(), + px, + Some(config.prune_backoff().as_secs()), + )], + ); + + // Assert no dials + assert_eq!( + gs.events + .iter() + .filter(|e| matches!(e, ToSwarm::Dial { .. })) + .count(), + 0 + ); + + // handle prune from peer peers[1] with px peers + let px = vec![PeerInfo { + peer_id: Some(PeerId::random()), + }]; + gs.handle_prune( + &peers[1], + vec![( + topics[0].clone(), + px, + Some(config.prune_backoff().as_secs()), + )], + ); + + // assert there are dials now + assert!( + gs.events + .iter() + .filter(|e| matches!(e, ToSwarm::Dial { .. })) + .count() + > 0 + ); +} + +#[test] +fn test_keep_best_scoring_peers_on_oversubscription() { + let config = ConfigBuilder::default() + .mesh_n_low(15) + .mesh_n(30) + .mesh_n_high(60) + .retain_scores(29) + .build() + .unwrap(); + + let mesh_n_high = config.mesh_n_high(); + + let (mut gs, peers, _queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(mesh_n_high) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + for peer in &peers { + gs.handle_graft(peer, topics.clone()); + } + + // assign scores to peers equalling their index + + // set random positive scores + for (index, peer) in peers.iter().enumerate() { + gs.set_application_score(peer, index as f64); + } + + assert_eq!(gs.mesh[&topics[0]].len(), mesh_n_high); + + // heartbeat to prune some peers + gs.heartbeat(); + + assert_eq!(gs.mesh[&topics[0]].len(), config.mesh_n()); + + // mesh contains retain_scores best peers + assert!(gs.mesh[&topics[0]].is_superset( + &peers[(mesh_n_high - config.retain_scores())..] + .iter() + .cloned() + .collect() + )); +} + +#[test] +fn test_scoring_p1() { + let config = Config::default(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 2.0, + time_in_mesh_quantum: Duration::from_millis(50), + time_in_mesh_cap: 10.0, + topic_weight: 0.7, + ..TopicScoreParams::default() + }; + peer_score_params + .topics + .insert(topic_hash, topic_params.clone()); + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with one peer + let (mut gs, peers, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + // sleep for 2 times the mesh_quantum + sleep(topic_params.time_in_mesh_quantum * 2); + // refresh scores + gs.as_peer_score_mut().refresh_scores(); + assert!( + gs.as_peer_score_mut().score_report(&peers[0]).score + >= 2.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, + "score should be at least 2 * time_in_mesh_weight * topic_weight" + ); + assert!( + gs.as_peer_score_mut().score_report(&peers[0]).score + < 3.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, + "score should be less than 3 * time_in_mesh_weight * topic_weight" + ); + + // sleep again for 2 times the mesh_quantum + sleep(topic_params.time_in_mesh_quantum * 2); + // refresh scores + gs.as_peer_score_mut().refresh_scores(); + assert!( + gs.as_peer_score_mut().score_report(&peers[0]).score + >= 2.0 * topic_params.time_in_mesh_weight * topic_params.topic_weight, + "score should be at least 4 * time_in_mesh_weight * topic_weight" + ); + + // sleep for enough periods to reach maximum + sleep(topic_params.time_in_mesh_quantum * (topic_params.time_in_mesh_cap - 3.0) as u32); + // refresh scores + gs.as_peer_score_mut().refresh_scores(); + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + topic_params.time_in_mesh_cap + * topic_params.time_in_mesh_weight + * topic_params.topic_weight, + "score should be exactly time_in_mesh_cap * time_in_mesh_weight * topic_weight" + ); +} + +#[test] +fn test_scoring_p2() { + let config = Config::default(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, // deactivate time in mesh + first_message_deliveries_weight: 2.0, + first_message_deliveries_cap: 10.0, + first_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..TopicScoreParams::default() + }; + peer_score_params + .topics + .insert(topic_hash, topic_params.clone()); + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with one peer + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut crate::Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + let m1 = random_message(&mut seq, &topics); + // peer 0 delivers message first + deliver_message(&mut gs, 0, m1.clone()); + // peer 1 delivers message second + deliver_message(&mut gs, 1, m1); + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + 1.0 * topic_params.first_message_deliveries_weight * topic_params.topic_weight, + "score should be exactly first_message_deliveries_weight * topic_weight" + ); + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[1]).score, + 0.0, + "there should be no score for second message deliveries * topic_weight" + ); + + // peer 2 delivers two new messages + deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); + deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[1]).score, + 2.0 * topic_params.first_message_deliveries_weight * topic_params.topic_weight, + "score should be exactly 2 * first_message_deliveries_weight * topic_weight" + ); + + // test decaying + gs.as_peer_score_mut().refresh_scores(); + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + 1.0 * topic_params.first_message_deliveries_decay + * topic_params.first_message_deliveries_weight + * topic_params.topic_weight, + "score should be exactly first_message_deliveries_decay * \ + first_message_deliveries_weight * topic_weight" + ); + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[1]).score, + 2.0 * topic_params.first_message_deliveries_decay + * topic_params.first_message_deliveries_weight + * topic_params.topic_weight, + "score should be exactly 2 * first_message_deliveries_decay * \ + first_message_deliveries_weight * topic_weight" + ); + + // test cap + for _ in 0..topic_params.first_message_deliveries_cap as u64 { + deliver_message(&mut gs, 1, random_message(&mut seq, &topics)); + } + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[1]).score, + topic_params.first_message_deliveries_cap + * topic_params.first_message_deliveries_weight + * topic_params.topic_weight, + "score should be exactly first_message_deliveries_cap * \ + first_message_deliveries_weight * topic_weight" + ); +} + +#[test] +fn test_scoring_p3() { + let config = Config::default(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, // deactivate time in mesh + first_message_deliveries_weight: 0.0, // deactivate first time deliveries + mesh_message_deliveries_weight: -2.0, + mesh_message_deliveries_decay: 0.9, + mesh_message_deliveries_cap: 10.0, + mesh_message_deliveries_threshold: 5.0, + mesh_message_deliveries_activation: Duration::from_secs(1), + mesh_message_deliveries_window: Duration::from_millis(100), + topic_weight: 0.7, + ..TopicScoreParams::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with two peers + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut crate::Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + let mut expected_message_deliveries = 0.0; + + // messages used to test window + let m1 = random_message(&mut seq, &topics); + let m2 = random_message(&mut seq, &topics); + + // peer 1 delivers m1 + deliver_message(&mut gs, 1, m1.clone()); + + // peer 0 delivers two message + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + expected_message_deliveries += 2.0; + + sleep(Duration::from_millis(60)); + + // peer 1 delivers m2 + deliver_message(&mut gs, 1, m2.clone()); + + sleep(Duration::from_millis(70)); + // peer 0 delivers m1 and m2 only m2 gets counted + deliver_message(&mut gs, 0, m1); + deliver_message(&mut gs, 0, m2); + expected_message_deliveries += 1.0; + + sleep(Duration::from_millis(900)); + + // message deliveries penalties get activated, peer 0 has only delivered 3 messages and + // therefore gets a penalty + gs.as_peer_score_mut().refresh_scores(); + expected_message_deliveries *= 0.9; // decay + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + (5f64 - expected_message_deliveries).powi(2) * -2.0 * 0.7 + ); + + // peer 0 delivers a lot of messages => message_deliveries should be capped at 10 + for _ in 0..20 { + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + } + + expected_message_deliveries = 10.0; + + assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); + + // apply 10 decays + for _ in 0..10 { + gs.as_peer_score_mut().refresh_scores(); + expected_message_deliveries *= 0.9; // decay + } + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + (5f64 - expected_message_deliveries).powi(2) * -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p3b() { + let config = ConfigBuilder::default() + .prune_backoff(Duration::from_millis(100)) + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + time_in_mesh_weight: 0.0, // deactivate time in mesh + first_message_deliveries_weight: 0.0, // deactivate first time deliveries + mesh_message_deliveries_weight: -2.0, + mesh_message_deliveries_decay: 0.9, + mesh_message_deliveries_cap: 10.0, + mesh_message_deliveries_threshold: 5.0, + mesh_message_deliveries_activation: Duration::from_secs(1), + mesh_message_deliveries_window: Duration::from_millis(100), + mesh_failure_penalty_weight: -3.0, + mesh_failure_penalty_decay: 0.95, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with one peer + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut crate::Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + let mut expected_message_deliveries = 0.0; + + // add some positive score + gs.as_peer_score_mut() + .set_application_score(&peers[0], 100.0); + + // peer 0 delivers two message + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + expected_message_deliveries += 2.0; + + sleep(Duration::from_millis(1050)); + + // activation kicks in + gs.as_peer_score_mut().refresh_scores(); + expected_message_deliveries *= 0.9; // decay + + // prune peer + gs.handle_prune(&peers[0], vec![(topics[0].clone(), vec![], None)]); + + // wait backoff + sleep(Duration::from_millis(130)); + + // regraft peer + gs.handle_graft(&peers[0], topics.clone()); + + // the score should now consider p3b + let mut expected_b3 = (5f64 - expected_message_deliveries).powi(2); + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + 100.0 + expected_b3 * -3.0 * 0.7 + ); + + // we can also add a new p3 to the score + + // peer 0 delivers one message + deliver_message(&mut gs, 0, random_message(&mut seq, &topics)); + expected_message_deliveries += 1.0; + + sleep(Duration::from_millis(1050)); + gs.as_peer_score_mut().refresh_scores(); + expected_message_deliveries *= 0.9; // decay + expected_b3 *= 0.95; + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + 100.0 + (expected_b3 * -3.0 + (5f64 - expected_message_deliveries).powi(2) * -2.0) * 0.7 + ); +} + +#[test] +fn test_scoring_p4_valid_message() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + // deactivate time in mesh + time_in_mesh_weight: 0.0, + // deactivate first time deliveries + first_message_deliveries_weight: 0.0, + // deactivate message deliveries + mesh_message_deliveries_weight: 0.0, + // deactivate mesh failure penalties + mesh_failure_penalty_weight: 0.0, + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with two peers + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut crate::Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + // peer 0 delivers valid message + let m1 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); + + assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); + + // message m1 gets validated + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Accept, + ); + + assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); +} + +#[test] +fn test_scoring_p4_invalid_signature() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + // deactivate time in mesh + time_in_mesh_weight: 0.0, + // deactivate first time deliveries + first_message_deliveries_weight: 0.0, + // deactivate message deliveries + mesh_message_deliveries_weight: 0.0, + // deactivate mesh failure penalties + mesh_failure_penalty_weight: 0.0, + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with one peer + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + + // peer 0 delivers message with invalid signature + let m = random_message(&mut seq, &topics); + + gs.on_connection_handler_event( + peers[0], + ConnectionId::new_unchecked(0), + HandlerEvent::Message { + rpc: RpcIn { + messages: vec![], + subscriptions: vec![], + control_msgs: vec![], + }, + invalid_messages: vec![(m, ValidationError::InvalidSignature)], + }, + ); + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p4_message_from_self() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + // deactivate time in mesh + time_in_mesh_weight: 0.0, + // deactivate first time deliveries + first_message_deliveries_weight: 0.0, + // deactivate message deliveries + mesh_message_deliveries_weight: 0.0, + // deactivate mesh failure penalties + mesh_failure_penalty_weight: 0.0, + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with two peers + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut crate::Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + // peer 0 delivers invalid message from self + let mut m = random_message(&mut seq, &topics); + m.source = Some(*gs.publish_config.get_own_id().unwrap()); + + deliver_message(&mut gs, 0, m); + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p4_ignored_message() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + // deactivate time in mesh + time_in_mesh_weight: 0.0, + // deactivate first time deliveries + first_message_deliveries_weight: 0.0, + // deactivate message deliveries + mesh_message_deliveries_weight: 0.0, + // deactivate mesh failure penalties + mesh_failure_penalty_weight: 0.0, + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with two peers + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut crate::Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + // peer 0 delivers ignored message + let m1 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + + assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); + + // message m1 gets ignored + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Ignore, + ); + + assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); +} + +#[test] +fn test_scoring_p4_application_invalidated_message() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + // deactivate time in mesh + time_in_mesh_weight: 0.0, + // deactivate first time deliveries + first_message_deliveries_weight: 0.0, + // deactivate message deliveries + mesh_message_deliveries_weight: 0.0, + // deactivate mesh failure penalties + mesh_failure_penalty_weight: 0.0, + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with two peers + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut crate::Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + // peer 0 delivers invalid message + let m1 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + + assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); + + // message m1 gets rejected + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Reject, + ); + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p4_application_invalid_message_from_two_peers() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + // deactivate time in mesh + time_in_mesh_weight: 0.0, + // deactivate first time deliveries + first_message_deliveries_weight: 0.0, + // deactivate message deliveries + mesh_message_deliveries_weight: 0.0, + // deactivate mesh failure penalties + mesh_failure_penalty_weight: 0.0, + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with two peers + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut crate::Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + // peer 0 delivers invalid message + let m1 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1.clone()).unwrap(); + + // peer 1 delivers same message + deliver_message(&mut gs, 1, m1); + + assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); + assert_eq!(gs.as_peer_score_mut().score_report(&peers[1]).score, 0.0); + + // message m1 gets rejected + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Reject, + ); + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + -2.0 * 0.7 + ); + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[1]).score, + -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p4_three_application_invalid_messages() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + // deactivate time in mesh + time_in_mesh_weight: 0.0, + // deactivate first time deliveries + first_message_deliveries_weight: 0.0, + // deactivate message deliveries + mesh_message_deliveries_weight: 0.0, + // deactivate mesh failure penalties + mesh_failure_penalty_weight: 0.0, + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with one peer + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut crate::Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + // peer 0 delivers two invalid message + let m1 = random_message(&mut seq, &topics); + let m2 = random_message(&mut seq, &topics); + let m3 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + deliver_message(&mut gs, 0, m2.clone()); + deliver_message(&mut gs, 0, m3.clone()); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); + + // Transform the inbound message + let message2 = &gs.data_transform.inbound_transform(m2).unwrap(); + // Transform the inbound message + let message3 = &gs.data_transform.inbound_transform(m3).unwrap(); + + assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); + + // messages gets rejected + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Reject, + ); + + gs.report_message_validation_result( + &config.message_id(message2), + &peers[0], + MessageAcceptance::Reject, + ); + + gs.report_message_validation_result( + &config.message_id(message3), + &peers[0], + MessageAcceptance::Reject, + ); + + // number of invalid messages gets squared + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + 9.0 * -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p4_decay() { + let config = ConfigBuilder::default() + .validate_messages() + .build() + .unwrap(); + let mut peer_score_params = PeerScoreParams::default(); + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let topic_params = TopicScoreParams { + // deactivate time in mesh + time_in_mesh_weight: 0.0, + // deactivate first time deliveries + first_message_deliveries_weight: 0.0, + // deactivate message deliveries + mesh_message_deliveries_weight: 0.0, + // deactivate mesh failure penalties + mesh_failure_penalty_weight: 0.0, + invalid_message_deliveries_weight: -2.0, + invalid_message_deliveries_decay: 0.9, + topic_weight: 0.7, + ..Default::default() + }; + peer_score_params.topics.insert(topic_hash, topic_params); + peer_score_params.app_specific_weight = 1.0; + let peer_score_thresholds = PeerScoreThresholds::default(); + + // build mesh with one peer + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(config.clone()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, peer_score_thresholds))) + .create_network(); + + let mut seq = 0; + let deliver_message = |gs: &mut crate::Behaviour, index: usize, msg: RawMessage| { + gs.handle_received_message(msg, &peers[index]); + }; + + // peer 0 delivers invalid message + let m1 = random_message(&mut seq, &topics); + deliver_message(&mut gs, 0, m1.clone()); + + // Transform the inbound message + let message1 = &gs.data_transform.inbound_transform(m1).unwrap(); + assert_eq!(gs.as_peer_score_mut().score_report(&peers[0]).score, 0.0); + + // message m1 gets rejected + gs.report_message_validation_result( + &config.message_id(message1), + &peers[0], + MessageAcceptance::Reject, + ); + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + -2.0 * 0.7 + ); + + // we decay + gs.as_peer_score_mut().refresh_scores(); + + // the number of invalids gets decayed to 0.9 and then squared in the score + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + 0.9 * 0.9 * -2.0 * 0.7 + ); +} + +#[test] +fn test_scoring_p5() { + let peer_score_params = PeerScoreParams { + app_specific_weight: 2.0, + ..PeerScoreParams::default() + }; + + // build mesh with one peer + let (mut gs, peers, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec!["test".into()]) + .to_subscribe(true) + .gs_config(Config::default()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) + .create_network(); + + gs.set_application_score(&peers[0], 1.1); + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + 1.1 * 2.0 + ); +} + +#[test] +fn test_scoring_p6() { + let peer_score_params = PeerScoreParams { + ip_colocation_factor_threshold: 5.0, + ip_colocation_factor_weight: -2.0, + ..Default::default() + }; + + let (mut gs, _, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(0) + .topics(vec![]) + .to_subscribe(false) + .gs_config(Config::default()) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) + .create_network(); + + // create 5 peers with the same ip + let addr = Multiaddr::from(Ipv4Addr::new(10, 1, 2, 3)); + let peers = [ + add_peer_with_addr(&mut gs, &[], false, false, addr.clone()).0, + add_peer_with_addr(&mut gs, &[], false, false, addr.clone()).0, + add_peer_with_addr(&mut gs, &[], true, false, addr.clone()).0, + add_peer_with_addr(&mut gs, &[], true, false, addr.clone()).0, + add_peer_with_addr(&mut gs, &[], true, true, addr.clone()).0, + ]; + + // create 4 other peers with other ip + let addr2 = Multiaddr::from(Ipv4Addr::new(10, 1, 2, 4)); + let others = [ + add_peer_with_addr(&mut gs, &[], false, false, addr2.clone()).0, + add_peer_with_addr(&mut gs, &[], false, false, addr2.clone()).0, + add_peer_with_addr(&mut gs, &[], true, false, addr2.clone()).0, + add_peer_with_addr(&mut gs, &[], true, false, addr2.clone()).0, + ]; + + // no penalties yet + for peer in peers.iter().chain(others.iter()) { + assert_eq!(gs.as_peer_score_mut().score_report(peer).score, 0.0); + } + + // add additional connection for 3 others with addr + for id in others.iter().take(3) { + gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id: *id, + connection_id: ConnectionId::new_unchecked(0), + endpoint: &libp2p_core::ConnectedPoint::Dialer { + address: addr.clone(), + role_override: Endpoint::Dialer, + port_use: PortUse::Reuse, + }, + failed_addresses: &[], + other_established: 0, + })); + } + + // penalties apply squared + for peer in peers.iter().chain(others.iter().take(3)) { + assert_eq!(gs.as_peer_score_mut().score_report(peer).score, 9.0 * -2.0); + } + // fourth other peer still no penalty + assert_eq!(gs.as_peer_score_mut().score_report(&others[3]).score, 0.0); + + // add additional connection for 3 of the peers to addr2 + for peer in peers.iter().take(3) { + gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id: *peer, + connection_id: ConnectionId::new_unchecked(0), + endpoint: &libp2p_core::ConnectedPoint::Dialer { + address: addr2.clone(), + role_override: Endpoint::Dialer, + port_use: PortUse::Reuse, + }, + failed_addresses: &[], + other_established: 1, + })); + } + + // double penalties for the first three of each + for peer in peers.iter().take(3).chain(others.iter().take(3)) { + assert_eq!( + gs.as_peer_score_mut().score_report(peer).score, + (9.0 + 4.0) * -2.0 + ); + } + + // single penalties for the rest + for peer in peers.iter().skip(3) { + assert_eq!(gs.as_peer_score_mut().score_report(peer).score, 9.0 * -2.0); + } + assert_eq!( + gs.as_peer_score_mut().score_report(&others[3]).score, + 4.0 * -2.0 + ); + + // two times same ip doesn't count twice + gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id: peers[0], + connection_id: ConnectionId::new_unchecked(0), + endpoint: &libp2p_core::ConnectedPoint::Dialer { + address: addr, + role_override: Endpoint::Dialer, + port_use: PortUse::Reuse, + }, + failed_addresses: &[], + other_established: 2, + })); + + // nothing changed + // double penalties for the first three of each + for peer in peers.iter().take(3).chain(others.iter().take(3)) { + assert_eq!( + gs.as_peer_score_mut().score_report(peer).score, + (9.0 + 4.0) * -2.0 + ); + } + + // single penalties for the rest + for peer in peers.iter().skip(3) { + assert_eq!(gs.as_peer_score_mut().score_report(peer).score, 9.0 * -2.0); + } + assert_eq!( + gs.as_peer_score_mut().score_report(&others[3]).score, + 4.0 * -2.0 + ); +} + +#[test] +fn test_scoring_p7_grafts_before_backoff() { + let config = ConfigBuilder::default() + .prune_backoff(Duration::from_millis(200)) + .graft_flood_threshold(Duration::from_millis(100)) + .build() + .unwrap(); + let peer_score_params = PeerScoreParams { + behaviour_penalty_weight: -2.0, + behaviour_penalty_decay: 0.9, + ..Default::default() + }; + + let (mut gs, peers, _queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(2) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, PeerScoreThresholds::default()))) + .create_network(); + + // remove peers from mesh and send prune to them => this adds a backoff for the peers + for peer in peers.iter().take(2) { + gs.mesh.get_mut(&topics[0]).unwrap().remove(peer); + gs.send_graft_prune( + HashMap::new(), + HashMap::from([(*peer, vec![topics[0].clone()])]), + HashSet::new(), + ); + } + + // wait 50 millisecs + sleep(Duration::from_millis(50)); + + // first peer tries to graft + gs.handle_graft(&peers[0], vec![topics[0].clone()]); + + // double behaviour penalty for first peer (squared) + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + 4.0 * -2.0 + ); + + // wait 100 millisecs + sleep(Duration::from_millis(100)); + + // second peer tries to graft + gs.handle_graft(&peers[1], vec![topics[0].clone()]); + + // single behaviour penalty for second peer + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[1]).score, + 1.0 * -2.0 + ); + + // test decay + gs.as_peer_score_mut().refresh_scores(); + + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[0]).score, + 4.0 * 0.9 * 0.9 * -2.0 + ); + assert_eq!( + gs.as_peer_score_mut().score_report(&peers[1]).score, + 1.0 * 0.9 * 0.9 * -2.0 + ); +} + +#[test] +fn test_opportunistic_grafting() { + let config = ConfigBuilder::default() + .mesh_n_low(3) + .mesh_n(5) + .mesh_n_high(7) + .mesh_outbound_min(0) // deactivate outbound handling + .opportunistic_graft_ticks(2) + .opportunistic_graft_peers(2) + .build() + .unwrap(); + let peer_score_params = PeerScoreParams { + app_specific_weight: 1.0, + ..Default::default() + }; + let thresholds = PeerScoreThresholds { + opportunistic_graft_threshold: 2.0, + ..Default::default() + }; + + let (mut gs, peers, _queues, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(5) + .topics(vec!["test".into()]) + .to_subscribe(false) + .gs_config(config) + .explicit(0) + .outbound(0) + .scoring(Some((peer_score_params, thresholds))) + .create_network(); + + // fill mesh with 5 peers + for peer in &peers { + gs.handle_graft(peer, topics.clone()); + } + + // add additional 5 peers + let others: Vec<_> = (0..5) + .map(|_| add_peer(&mut gs, &topics, false, false)) + .collect(); + + // currently mesh equals peers + assert_eq!(gs.mesh[&topics[0]], peers.iter().cloned().collect()); + + // give others high scores (but the first two have not high enough scores) + for (i, peer) in peers.iter().enumerate().take(5) { + gs.set_application_score(peer, 0.0 + i as f64); + } + + // set scores for peers in the mesh + for (i, (peer, _queue)) in others.iter().enumerate().take(5) { + gs.set_application_score(peer, 0.0 + i as f64); + } + + // this gives a median of exactly 2.0 => should not apply opportunistic grafting + gs.heartbeat(); + gs.heartbeat(); + + assert_eq!( + gs.mesh[&topics[0]].len(), + 5, + "should not apply opportunistic grafting" + ); + + // reduce middle score to 1.0 giving a median of 1.0 + gs.set_application_score(&peers[2], 1.0); + + // opportunistic grafting after two heartbeats + + gs.heartbeat(); + assert_eq!( + gs.mesh[&topics[0]].len(), + 5, + "should not apply opportunistic grafting after first tick" + ); + + gs.heartbeat(); + + assert_eq!( + gs.mesh[&topics[0]].len(), + 7, + "opportunistic grafting should have added 2 peers" + ); + + assert!( + gs.mesh[&topics[0]].is_superset(&peers.iter().cloned().collect()), + "old peers are still part of the mesh" + ); + + assert!( + gs.mesh[&topics[0]].is_disjoint(&others.iter().map(|(p, _)| p).cloned().take(2).collect()), + "peers below or equal to median should not be added in opportunistic grafting" + ); +} + +#[test] +fn test_subscribe_and_graft_with_negative_score() { + // simulate a communication between two gossipsub instances + let (mut gs1, _, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .topics(vec!["test".into()]) + .scoring(Some(( + PeerScoreParams::default(), + PeerScoreThresholds::default(), + ))) + .create_network(); + + let (mut gs2, _, queues, _) = DefaultBehaviourTestBuilder::default().create_network(); + + let connection_id = ConnectionId::new_unchecked(0); + + let topic = Topic::new("test"); + + let (p2, _queue1) = add_peer(&mut gs1, &Vec::new(), true, false); + let (p1, _queue2) = add_peer(&mut gs2, &topic_hashes, false, false); + + // add penalty to peer p2 + gs1.as_peer_score_mut().add_penalty(&p2, 1); + + let original_score = gs1.as_peer_score_mut().score_report(&p2).score; + + // subscribe to topic in gs2 + gs2.subscribe(&topic).unwrap(); + + let forward_messages_to_p1 = |gs1: &mut crate::Behaviour<_, _>, + p1: PeerId, + p2: PeerId, + connection_id: ConnectionId, + queues: HashMap| + -> HashMap { + let new_queues = HashMap::new(); + for (peer_id, mut receiver_queue) in queues.into_iter() { + match receiver_queue.try_pop() { + Some(rpc) if peer_id == p1 => { + gs1.on_connection_handler_event( + p2, + connection_id, + HandlerEvent::Message { + rpc: proto_to_message(&rpc.into_protobuf()), + invalid_messages: vec![], + }, + ); + } + _ => {} + } + } + new_queues + }; + + // forward the subscribe message + let queues = forward_messages_to_p1(&mut gs1, p1, p2, connection_id, queues); + + // heartbeats on both + gs1.heartbeat(); + gs2.heartbeat(); + + // forward messages again + forward_messages_to_p1(&mut gs1, p1, p2, connection_id, queues); + + // nobody got penalized + assert!(gs1.as_peer_score_mut().score_report(&p2).score >= original_score); +} diff --git a/protocols/gossipsub/src/behaviour/tests/subscription.rs b/protocols/gossipsub/src/behaviour/tests/subscription.rs new file mode 100644 index 00000000000..e9d09f16c66 --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/subscription.rs @@ -0,0 +1,506 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Tests for subscription, unsubscription, and join functionality. + +use std::collections::HashMap; + +use hashlink::LinkedHashMap; +use libp2p_core::ConnectedPoint; + +use super::{flush_events, DefaultBehaviourTestBuilder}; +use crate::{ + behaviour::tests::BehaviourTestBuilder, + subscription_filter::WhitelistSubscriptionFilter, + transform::IdentityTransform, + types::{PeerDetails, PeerKind, RpcOut, Subscription, SubscriptionAction}, + IdentTopic as Topic, +}; + +#[test] +/// Test local node subscribing to a topic +fn test_subscribe() { + // The node should: + // - Create an empty vector in mesh[topic] + // - Send subscription request to all peers + // - run JOIN(topic) + + let subscribe_topic = vec![String::from("test_subscribe")]; + let (gs, _, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(subscribe_topic) + .to_subscribe(true) + .create_network(); + + assert!( + gs.mesh.contains_key(&topic_hashes[0]), + "Subscribe should add a new entry to the mesh[topic] hashmap" + ); + + // collect all the subscriptions + let subscriptions = queues + .into_values() + .fold(0, |mut collected_subscriptions, mut queue| { + while !queue.is_empty() { + if let Some(RpcOut::Subscribe(_)) = queue.try_pop() { + collected_subscriptions += 1 + } + } + collected_subscriptions + }); + + // we sent a subscribe to all known peers + assert_eq!(subscriptions, 20); +} + +/// Test unsubscribe. +#[test] +fn test_unsubscribe() { + // Unsubscribe should: + // - Remove the mesh entry for topic + // - Send UNSUBSCRIBE to all known peers + // - Call Leave + + let topic_strings = vec![String::from("topic1"), String::from("topic2")]; + let topics = topic_strings + .iter() + .map(|t| Topic::new(t.clone())) + .collect::>(); + + // subscribe to topic_strings + let (mut gs, _, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(topic_strings) + .to_subscribe(true) + .create_network(); + + for topic_hash in &topic_hashes { + assert!( + gs.connected_peers + .values() + .any(|p| p.topics.contains(topic_hash)), + "Topic_peers contain a topic entry" + ); + assert!( + gs.mesh.contains_key(topic_hash), + "mesh should contain a topic entry" + ); + } + + // unsubscribe from both topics + assert!( + gs.unsubscribe(&topics[0]), + "should be able to unsubscribe successfully from each topic", + ); + assert!( + gs.unsubscribe(&topics[1]), + "should be able to unsubscribe successfully from each topic", + ); + + // collect all the subscriptions + let subscriptions = queues + .into_values() + .fold(0, |mut collected_subscriptions, mut queue| { + while !queue.is_empty() { + if let Some(RpcOut::Subscribe(_)) = queue.try_pop() { + collected_subscriptions += 1 + } + } + collected_subscriptions + }); + + // we sent a unsubscribe to all known peers, for two topics + assert_eq!(subscriptions, 40); + + // check we clean up internal structures + for topic_hash in &topic_hashes { + assert!( + !gs.mesh.contains_key(topic_hash), + "All topics should have been removed from the mesh" + ); + } +} + +/// Test JOIN(topic) functionality. +#[test] +fn test_join() { + use libp2p_core::{transport::PortUse, Endpoint, Multiaddr}; + use libp2p_identity::PeerId; + use libp2p_swarm::{behaviour::ConnectionEstablished, ConnectionId, NetworkBehaviour}; + + use crate::{behaviour::FromSwarm, queue::Queue}; + + // The Join function should: + // - Remove peers from fanout[topic] + // - Add any fanout[topic] peers to the mesh (up to mesh_n) + // - Fill up to mesh_n peers from known gossipsub peers in the topic + // - Send GRAFT messages to all nodes added to the mesh + + // This test is not an isolated unit test, rather it uses higher level, + // subscribe/unsubscribe to perform the test. + + let topic_strings = vec![String::from("topic1"), String::from("topic2")]; + let topics = topic_strings + .iter() + .map(|t| Topic::new(t.clone())) + .collect::>(); + + let (mut gs, _, mut queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(topic_strings) + .to_subscribe(true) + .create_network(); + + // Flush previous GRAFT messages. + queues = flush_events(&mut gs, queues); + + // unsubscribe, then call join to invoke functionality + assert!( + gs.unsubscribe(&topics[0]), + "should be able to unsubscribe successfully" + ); + assert!( + gs.unsubscribe(&topics[1]), + "should be able to unsubscribe successfully" + ); + + // re-subscribe - there should be peers associated with the topic + assert!( + gs.subscribe(&topics[0]).unwrap(), + "should be able to subscribe successfully" + ); + + // should have added mesh_n nodes to the mesh + assert!( + gs.mesh.get(&topic_hashes[0]).unwrap().len() == 6, + "Should have added 6 nodes to the mesh" + ); + + fn count_grafts(queues: HashMap) -> (usize, HashMap) { + let mut new_queues = HashMap::new(); + let mut acc = 0; + + for (peer_id, mut queue) in queues.into_iter() { + while !queue.is_empty() { + if let Some(RpcOut::Graft(_)) = queue.try_pop() { + acc += 1; + } + } + new_queues.insert(peer_id, queue); + } + (acc, new_queues) + } + + // there should be mesh_n GRAFT messages. + let (graft_messages, mut queues) = count_grafts(queues); + + assert_eq!( + graft_messages, 6, + "There should be 6 grafts messages sent to peers" + ); + + // verify fanout nodes + // add 3 random peers to the fanout[topic1] + gs.fanout + .insert(topic_hashes[1].clone(), Default::default()); + let mut new_peers: Vec = vec![]; + + for _ in 0..3 { + let random_peer = PeerId::random(); + // inform the behaviour of a new peer + let address = "/ip4/127.0.0.1".parse::().unwrap(); + gs.handle_established_inbound_connection( + ConnectionId::new_unchecked(0), + random_peer, + &address, + &address, + ) + .unwrap(); + let queue = Queue::new(gs.config.connection_handler_queue_len()); + let receiver_queue = queue.clone(); + let connection_id = ConnectionId::new_unchecked(0); + gs.connected_peers.insert( + random_peer, + PeerDetails { + kind: PeerKind::Floodsub, + outbound: false, + connections: vec![connection_id], + topics: Default::default(), + messages: queue, + dont_send: LinkedHashMap::new(), + }, + ); + queues.insert(random_peer, receiver_queue); + + gs.on_swarm_event(FromSwarm::ConnectionEstablished(ConnectionEstablished { + peer_id: random_peer, + connection_id, + endpoint: &ConnectedPoint::Dialer { + address, + role_override: Endpoint::Dialer, + port_use: PortUse::Reuse, + }, + failed_addresses: &[], + other_established: 0, + })); + + // add the new peer to the fanout + let fanout_peers = gs.fanout.get_mut(&topic_hashes[1]).unwrap(); + fanout_peers.insert(random_peer); + new_peers.push(random_peer); + } + + // subscribe to topic1 + gs.subscribe(&topics[1]).unwrap(); + + // the three new peers should have been added, along with 3 more from the pool. + assert!( + gs.mesh.get(&topic_hashes[1]).unwrap().len() == 6, + "Should have added 6 nodes to the mesh" + ); + let mesh_peers = gs.mesh.get(&topic_hashes[1]).unwrap(); + for new_peer in new_peers { + assert!( + mesh_peers.contains(&new_peer), + "Fanout peer should be included in the mesh" + ); + } + + // there should now 6 graft messages to be sent + let (graft_messages, _) = count_grafts(queues); + + assert_eq!( + graft_messages, 6, + "There should be 6 grafts messages sent to peers" + ); +} + +/// Test the gossipsub NetworkBehaviour peer connection logic. +/// Renamed from test_inject_connected +#[test] +fn test_peer_added_on_connection() { + let (gs, peers, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(vec![String::from("topic1"), String::from("topic2")]) + .to_subscribe(true) + .create_network(); + + // check that our subscriptions are sent to each of the peers + // collect all the SendEvents + let subscriptions = queues.into_iter().fold( + HashMap::>::new(), + |mut collected_subscriptions, (peer, mut queue)| { + while !queue.is_empty() { + if let Some(RpcOut::Subscribe(topic)) = queue.try_pop() { + let mut peer_subs = collected_subscriptions.remove(&peer).unwrap_or_default(); + peer_subs.push(topic.into_string()); + collected_subscriptions.insert(peer, peer_subs); + } + } + collected_subscriptions + }, + ); + + // check that there are two subscriptions sent to each peer + for peer_subs in subscriptions.values() { + assert!(peer_subs.contains(&String::from("topic1"))); + assert!(peer_subs.contains(&String::from("topic2"))); + assert_eq!(peer_subs.len(), 2); + } + + // check that there are 20 send events created + assert_eq!(subscriptions.len(), 20); + + // should add the new peers to `peer_topics` with an empty vec as a gossipsub node + for peer in peers { + let peer = gs.connected_peers.get(&peer).unwrap(); + assert!( + peer.topics == topic_hashes.iter().cloned().collect(), + "The topics for each node should all topics" + ); + } +} + +/// Test subscription handling +#[test] +fn test_handle_received_subscriptions() { + use std::collections::BTreeSet; + + use libp2p_identity::PeerId; + + // For every subscription: + // SUBSCRIBE: - Add subscribed topic to peer_topics for peer. + // - Add peer to topics_peer. + // UNSUBSCRIBE - Remove topic from peer_topics for peer. + // - Remove peer from topic_peers. + + let topics = ["topic1", "topic2", "topic3", "topic4"] + .iter() + .map(|&t| String::from(t)) + .collect(); + let (mut gs, peers, _queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(20) + .topics(topics) + .to_subscribe(false) + .create_network(); + + // The first peer sends 3 subscriptions and 1 unsubscription + let mut subscriptions = topic_hashes[..3] + .iter() + .map(|topic_hash| Subscription { + action: SubscriptionAction::Subscribe, + topic_hash: topic_hash.clone(), + }) + .collect::>(); + + subscriptions.push(Subscription { + action: SubscriptionAction::Unsubscribe, + topic_hash: topic_hashes[topic_hashes.len() - 1].clone(), + }); + + let unknown_peer = PeerId::random(); + // process the subscriptions + // first and second peers send subscriptions + gs.handle_received_subscriptions(&subscriptions, &peers[0]); + gs.handle_received_subscriptions(&subscriptions, &peers[1]); + // unknown peer sends the same subscriptions + gs.handle_received_subscriptions(&subscriptions, &unknown_peer); + + // verify the result + + let peer = gs.connected_peers.get(&peers[0]).unwrap(); + assert!( + peer.topics + == topic_hashes + .iter() + .take(3) + .cloned() + .collect::>(), + "First peer should be subscribed to three topics" + ); + let peer1 = gs.connected_peers.get(&peers[1]).unwrap(); + assert!( + peer1.topics + == topic_hashes + .iter() + .take(3) + .cloned() + .collect::>(), + "Second peer should be subscribed to three topics" + ); + + assert!( + !gs.connected_peers.contains_key(&unknown_peer), + "Unknown peer should not have been added" + ); + + for topic_hash in topic_hashes[..3].iter() { + let topic_peers = gs + .connected_peers + .iter() + .filter(|(_, p)| p.topics.contains(topic_hash)) + .map(|(peer_id, _)| *peer_id) + .collect::>(); + assert!( + topic_peers == peers[..2].iter().cloned().collect(), + "Two peers should be added to the first three topics" + ); + } + + // Peer 0 unsubscribes from the first topic + + gs.handle_received_subscriptions( + &[Subscription { + action: SubscriptionAction::Unsubscribe, + topic_hash: topic_hashes[0].clone(), + }], + &peers[0], + ); + + let peer = gs.connected_peers.get(&peers[0]).unwrap(); + assert!( + peer.topics == topic_hashes[1..3].iter().cloned().collect::>(), + "Peer should be subscribed to two topics" + ); + + // only gossipsub at the moment + let topic_peers = gs + .connected_peers + .iter() + .filter(|(_, p)| p.topics.contains(&topic_hashes[0])) + .map(|(peer_id, _)| *peer_id) + .collect::>(); + + assert!( + topic_peers == peers[1..2].iter().cloned().collect(), + "Only the second peers should be in the first topic" + ); +} + +#[test] +fn test_subscribe_to_invalid_topic() { + use std::collections::HashSet; + + let t1 = Topic::new("t1"); + let t2 = Topic::new("t2"); + let (mut gs, _, _, _) = BehaviourTestBuilder::::default() + .subscription_filter(WhitelistSubscriptionFilter( + vec![t1.hash()].into_iter().collect::>(), + )) + .create_network(); + + assert!(gs.subscribe(&t1).is_ok()); + assert!(gs.subscribe(&t2).is_err()); +} + +/// Renamed from test_public_api +#[test] +fn test_subscription_public_api() { + use std::collections::BTreeSet; + + use crate::topic::TopicHash; + + let (gs, peers, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(4) + .topics(vec![String::from("topic1")]) + .to_subscribe(true) + .create_network(); + let peers = peers.into_iter().collect::>(); + + assert_eq!( + gs.topics().cloned().collect::>(), + topic_hashes, + "Expected topics to match registered topic." + ); + + assert_eq!( + gs.mesh_peers(&TopicHash::from_raw("topic1")) + .cloned() + .collect::>(), + peers, + "Expected peers for a registered topic to contain all peers." + ); + + assert_eq!( + gs.all_mesh_peers().cloned().collect::>(), + peers, + "Expected all_peers to contain all peers." + ); +} diff --git a/protocols/gossipsub/src/behaviour/tests/topic_config.rs b/protocols/gossipsub/src/behaviour/tests/topic_config.rs new file mode 100644 index 00000000000..ff14c6f0d01 --- /dev/null +++ b/protocols/gossipsub/src/behaviour/tests/topic_config.rs @@ -0,0 +1,802 @@ +// Copyright 2025 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Tests for topic-specific configuration. + +use std::collections::HashMap; + +use asynchronous_codec::{Decoder, Encoder}; +use bytes::BytesMut; +use libp2p_swarm::{ConnectionId, NetworkBehaviour, ToSwarm}; + +use super::DefaultBehaviourTestBuilder; +use crate::{ + config::{Config, ConfigBuilder, TopicMeshConfig}, + error::ValidationError, + handler::HandlerEvent, + protocol::GossipsubCodec, + rpc_proto::proto, + topic::TopicHash, + types::{RawMessage, RpcIn, RpcOut}, + Event, IdentTopic as Topic, PublishError, ValidationMode, +}; + +/// Test that specific topic configurations are correctly applied +#[test] +fn test_topic_specific_config() { + let topic_hash1 = Topic::new("topic1").hash(); + let topic_hash2 = Topic::new("topic2").hash(); + + let topic_config1 = TopicMeshConfig { + mesh_n: 5, + mesh_n_low: 3, + mesh_n_high: 10, + mesh_outbound_min: 2, + }; + + let topic_config2 = TopicMeshConfig { + mesh_n: 8, + mesh_n_low: 4, + mesh_n_high: 12, + mesh_outbound_min: 3, + }; + + let config = ConfigBuilder::default() + .set_topic_config(topic_hash1.clone(), topic_config1) + .set_topic_config(topic_hash2.clone(), topic_config2) + .build() + .unwrap(); + + assert_eq!(config.mesh_n_for_topic(&topic_hash1), 5); + assert_eq!(config.mesh_n_low_for_topic(&topic_hash1), 3); + assert_eq!(config.mesh_n_high_for_topic(&topic_hash1), 10); + assert_eq!(config.mesh_outbound_min_for_topic(&topic_hash1), 2); + + assert_eq!(config.mesh_n_for_topic(&topic_hash2), 8); + assert_eq!(config.mesh_n_low_for_topic(&topic_hash2), 4); + assert_eq!(config.mesh_n_high_for_topic(&topic_hash2), 12); + assert_eq!(config.mesh_outbound_min_for_topic(&topic_hash2), 3); + + let topic_hash3 = TopicHash::from_raw("topic3"); + + assert_eq!(config.mesh_n_for_topic(&topic_hash3), config.mesh_n()); + assert_eq!( + config.mesh_n_low_for_topic(&topic_hash3), + config.mesh_n_low() + ); + assert_eq!( + config.mesh_n_high_for_topic(&topic_hash3), + config.mesh_n_high() + ); + assert_eq!( + config.mesh_outbound_min_for_topic(&topic_hash3), + config.mesh_outbound_min() + ); +} + +/// Test mesh maintenance with topic-specific configurations +#[test] +fn test_topic_mesh_maintenance_with_specific_config() { + let topic1_hash = TopicHash::from_raw("topic1"); + let topic2_hash = TopicHash::from_raw("topic2"); + + let topic_config1 = TopicMeshConfig { + mesh_n: 4, + mesh_n_low: 2, + mesh_n_high: 6, + mesh_outbound_min: 1, + }; + + let topic_config2 = TopicMeshConfig { + mesh_n: 8, + mesh_n_low: 4, + mesh_n_high: 12, + mesh_outbound_min: 3, + }; + + let config = ConfigBuilder::default() + .set_topic_config(topic1_hash, topic_config1) + .set_topic_config(topic2_hash, topic_config2) + .build() + .unwrap(); + + let (mut gs, _, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(15) + .topics(vec!["topic1".into(), "topic2".into()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + assert_eq!( + gs.mesh.get(&topic_hashes[0]).unwrap().len(), + 2, + "topic1 should have mesh_n 2 peers" + ); + assert_eq!( + gs.mesh.get(&topic_hashes[1]).unwrap().len(), + 4, + "topic2 should have mesh_n 4 peers" + ); + + // run a heartbeat + gs.heartbeat(); + + assert_eq!( + gs.mesh.get(&topic_hashes[0]).unwrap().len(), + 2, + "topic1 should maintain mesh_n 2 peers after heartbeat" + ); + assert_eq!( + gs.mesh.get(&topic_hashes[1]).unwrap().len(), + 4, + "topic2 should maintain mesh_n 4 peers after heartbeat" + ); +} + +/// Test mesh addition with topic-specific configuration +#[test] +fn test_mesh_addition_with_topic_config() { + let topic = String::from("topic1"); + let topic_hash = TopicHash::from_raw(topic.clone()); + + let topic_config = TopicMeshConfig { + mesh_n: 6, + mesh_n_low: 3, + mesh_n_high: 9, + mesh_outbound_min: 2, + }; + + let config = ConfigBuilder::default() + .set_topic_config(topic_hash.clone(), topic_config.clone()) + .build() + .unwrap(); + + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(config.mesh_n_for_topic(&topic_hash) + 1) + .topics(vec![topic]) + .to_subscribe(true) + .gs_config(config.clone()) + .create_network(); + + let to_remove_peers = 1; + + for peer in peers.iter().take(to_remove_peers) { + gs.handle_prune( + peer, + topics.iter().map(|h| (h.clone(), vec![], None)).collect(), + ); + } + + assert_eq!( + gs.mesh.get(&topics[0]).unwrap().len(), + config.mesh_n_low_for_topic(&topic_hash) - 1 + ); + + // run a heartbeat + gs.heartbeat(); + + // Peers should be added to reach mesh_n + assert_eq!( + gs.mesh.get(&topics[0]).unwrap().len(), + config.mesh_n_for_topic(&topic_hash) + ); +} + +/// Test mesh subtraction with topic-specific configuration +#[test] +fn test_mesh_subtraction_with_topic_config() { + let topic = String::from("topic1"); + let topic_hash = TopicHash::from_raw(topic.clone()); + + let mesh_n = 5; + let mesh_n_high = 7; + + let topic_config = TopicMeshConfig { + mesh_n, + mesh_n_high, + mesh_n_low: 3, + mesh_outbound_min: 2, + }; + + let config = ConfigBuilder::default() + .set_topic_config(topic_hash.clone(), topic_config) + .build() + .unwrap(); + + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(mesh_n_high) + .topics(vec![topic]) + .to_subscribe(true) + .gs_config(config.clone()) + .outbound(mesh_n_high) + .create_network(); + + // graft all peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + assert_eq!( + gs.mesh.get(&topics[0]).unwrap().len(), + mesh_n_high, + "Initially all peers should be in the mesh" + ); + + // run a heartbeat + gs.heartbeat(); + + // Peers should be removed to reach mesh_n + assert_eq!( + gs.mesh.get(&topics[0]).unwrap().len(), + 5, + "After heartbeat, mesh should be reduced to mesh_n 5 peers" + ); +} + +/// Tests that if a mesh reaches `mesh_n_high`, +/// but is only composed of outbound peers, it is not reduced to `mesh_n`. +#[test] +fn test_mesh_subtraction_with_topic_config_min_outbound() { + let topic = String::from("topic1"); + let topic_hash = TopicHash::from_raw(topic.clone()); + + let mesh_n = 5; + let mesh_n_high = 7; + + let topic_config = TopicMeshConfig { + mesh_n, + mesh_n_high, + mesh_n_low: 3, + mesh_outbound_min: 7, + }; + + let config = ConfigBuilder::default() + .set_topic_config(topic_hash.clone(), topic_config) + .build() + .unwrap(); + + let peer_no = 12; + + // make all outbound connections. + let (mut gs, peers, _, topics) = DefaultBehaviourTestBuilder::default() + .peer_no(peer_no) + .topics(vec![topic]) + .to_subscribe(true) + .gs_config(config.clone()) + .outbound(peer_no) + .create_network(); + + // graft all peers + for peer in peers { + gs.handle_graft(&peer, topics.clone()); + } + + assert_eq!( + gs.mesh.get(&topics[0]).unwrap().len(), + mesh_n_high, + "Initially mesh should be {mesh_n_high}" + ); + + // run a heartbeat + gs.heartbeat(); + + assert_eq!( + gs.mesh.get(&topics[0]).unwrap().len(), + mesh_n_high, + "After heartbeat, mesh should still be {mesh_n_high} as these are all outbound peers" + ); +} + +/// Test behavior with multiple topics having different configs +#[test] +fn test_multiple_topics_with_different_configs() { + let topic1 = String::from("topic1"); + let topic2 = String::from("topic2"); + let topic3 = String::from("topic3"); + + let topic_hash1 = TopicHash::from_raw(topic1.clone()); + let topic_hash2 = TopicHash::from_raw(topic2.clone()); + let topic_hash3 = TopicHash::from_raw(topic3.clone()); + + let config1 = TopicMeshConfig { + mesh_n: 4, + mesh_n_low: 3, + mesh_n_high: 6, + mesh_outbound_min: 1, + }; + + let config2 = TopicMeshConfig { + mesh_n: 6, + mesh_n_low: 4, + mesh_n_high: 9, + mesh_outbound_min: 2, + }; + + let config3 = TopicMeshConfig { + mesh_n: 9, + mesh_n_low: 6, + mesh_n_high: 13, + mesh_outbound_min: 3, + }; + + let config = ConfigBuilder::default() + .set_topic_config(topic_hash1.clone(), config1) + .set_topic_config(topic_hash2.clone(), config2) + .set_topic_config(topic_hash3.clone(), config3) + .build() + .unwrap(); + + // Create network with many peers and three topics + let (mut gs, _, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(35) + .topics(vec![topic1, topic2, topic3]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + // Check that mesh sizes match each topic's config + assert_eq!( + gs.mesh.get(&topic_hashes[0]).unwrap().len(), + 3, + "topic1 should have 3 peers" + ); + assert_eq!( + gs.mesh.get(&topic_hashes[1]).unwrap().len(), + 4, + "topic2 should have 4 peers" + ); + assert_eq!( + gs.mesh.get(&topic_hashes[2]).unwrap().len(), + 6, + "topic3 should have 6 peers" + ); + + // run a heartbeat + gs.heartbeat(); + + // Verify mesh sizes remain correct after maintenance. The mesh parameters are > mesh_n_low and + // < mesh_n_high so the implementation will maintain the mesh at mesh_n_low. + assert_eq!( + gs.mesh.get(&topic_hashes[0]).unwrap().len(), + 3, + "topic1 should maintain 3 peers after heartbeat" + ); + assert_eq!( + gs.mesh.get(&topic_hashes[1]).unwrap().len(), + 4, + "topic2 should maintain 4 peers after heartbeat" + ); + assert_eq!( + gs.mesh.get(&topic_hashes[2]).unwrap().len(), + 6, + "topic3 should maintain 6 peers after heartbeat" + ); + + // Unsubscribe from topic1 + assert!( + gs.unsubscribe(&Topic::new(topic_hashes[0].to_string())), + "Should unsubscribe successfully" + ); + + // verify it's removed from mesh + assert!( + !gs.mesh.contains_key(&topic_hashes[0]), + "topic1 should be removed from mesh after unsubscribe" + ); + + // re-subscribe to topic1 + assert!( + gs.subscribe(&Topic::new(topic_hashes[0].to_string())) + .unwrap(), + "Should subscribe successfully" + ); + + // Verify mesh is recreated with correct size + assert_eq!( + gs.mesh.get(&topic_hashes[0]).unwrap().len(), + 4, + "topic1 should have mesh_n 4 peers after re-subscribe" + ); +} + +/// Test fanout behavior with topic-specific configuration +#[test] +fn test_fanout_with_topic_config() { + let topic = String::from("topic1"); + let topic_hash = TopicHash::from_raw(topic.clone()); + + let topic_config = TopicMeshConfig { + mesh_n: 4, + mesh_n_low: 2, + mesh_n_high: 7, + mesh_outbound_min: 1, + }; + + // turn off flood publish to test fanout behaviour + let config = ConfigBuilder::default() + .flood_publish(false) + .set_topic_config(topic_hash.clone(), topic_config) + .build() + .unwrap(); + + let (mut gs, _, queues, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(10) // More than mesh_n + .topics(vec![topic.clone()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + assert!( + gs.unsubscribe(&Topic::new(topic.clone())), + "Should unsubscribe successfully" + ); + + let publish_data = vec![0; 42]; + gs.publish(Topic::new(topic), publish_data).unwrap(); + + // Check that fanout size matches the topic-specific mesh_n + assert_eq!( + gs.fanout.get(&topic_hashes[0]).unwrap().len(), + 4, + "Fanout should contain topic-specific mesh_n 4 peers for this topic" + ); + + // Collect publish messages + let publishes = queues + .into_values() + .fold(vec![], |mut collected_publish, mut queue| { + while !queue.is_empty() { + if let Some(RpcOut::Publish { message, .. }) = queue.try_pop() { + collected_publish.push(message); + } + } + collected_publish + }); + + // Verify sent to topic-specific mesh_n number of peers + assert_eq!( + publishes.len(), + 4, + "Should send a publish message to topic-specific mesh_n 4 fanout peers" + ); +} + +#[test] +fn test_publish_message_with_default_transmit_size_config() { + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + + let config = ConfigBuilder::default() + .max_transmit_size_for_topic(Config::default_max_transmit_size(), topic_hash.clone()) + .validation_mode(ValidationMode::Strict) + .build() + .unwrap(); + + let (mut gs, _, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(10) + .topics(vec!["test".to_string()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + let data = vec![0; 1024]; + + let result = gs.publish(topic.clone(), data); + assert!( + result.is_ok(), + "Expected successful publish within size limit" + ); + let msg_id = result.unwrap(); + assert!( + gs.mcache.get(&msg_id).is_some(), + "Message should be in cache" + ); +} + +#[test] +fn test_publish_large_message_with_default_transmit_size_config() { + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + + let config = ConfigBuilder::default() + .max_transmit_size_for_topic(Config::default_max_transmit_size(), topic_hash.clone()) + .validation_mode(ValidationMode::Strict) + .build() + .unwrap(); + + let (mut gs, _, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(10) + .topics(vec!["test".to_string()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + let data = vec![0; Config::default_max_transmit_size() + 1]; + + let result = gs.publish(topic.clone(), data); + assert!( + matches!(result, Err(PublishError::MessageTooLarge)), + "Expected MessageTooLarge error for oversized message" + ); +} + +#[test] +fn test_publish_message_with_specific_transmit_size_config() { + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + + let max_topic_transmit_size = 2000; + let config = ConfigBuilder::default() + .max_transmit_size_for_topic(max_topic_transmit_size, topic_hash.clone()) + .validation_mode(ValidationMode::Strict) + .build() + .unwrap(); + + let (mut gs, _, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(10) + .topics(vec!["test".to_string()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + let data = vec![0; 1024]; + + let result = gs.publish(topic.clone(), data); + assert!( + result.is_ok(), + "Expected successful publish within topic-specific size limit" + ); + let msg_id = result.unwrap(); + assert!( + gs.mcache.get(&msg_id).is_some(), + "Message should be in cache" + ); +} + +#[test] +fn test_publish_large_message_with_specific_transmit_size_config() { + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + + let max_topic_transmit_size = 2048; + let config = ConfigBuilder::default() + .max_transmit_size_for_topic(max_topic_transmit_size, topic_hash.clone()) + .validation_mode(ValidationMode::Strict) + .build() + .unwrap(); + + let (mut gs, _, _, _) = DefaultBehaviourTestBuilder::default() + .peer_no(10) + .topics(vec!["test".to_string()]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + let data = vec![0; 2049]; + + let result = gs.publish(topic.clone(), data); + assert!( + matches!(result, Err(PublishError::MessageTooLarge)), + "Expected MessageTooLarge error for oversized message with topic-specific config" + ); +} + +#[test] +fn test_validation_error_message_size_too_large_topic_specific() { + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let max_size = 2048; + + let config = ConfigBuilder::default() + .max_transmit_size_for_topic(max_size, topic_hash.clone()) + .validation_mode(ValidationMode::None) + .build() + .unwrap(); + + let (mut gs, peers, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec![String::from("test")]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + let data = vec![0u8; max_size + 1]; + let raw_message = RawMessage { + source: Some(peers[0]), + data, + sequence_number: Some(1u64), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: false, + }; + + gs.on_connection_handler_event( + peers[0], + ConnectionId::new_unchecked(0), + HandlerEvent::Message { + rpc: RpcIn { + messages: vec![raw_message], + subscriptions: vec![], + control_msgs: vec![], + }, + invalid_messages: vec![], + }, + ); + + let event = gs.events.pop_front().expect("Event should be generated"); + match event { + ToSwarm::GenerateEvent(Event::Message { + propagation_source, + message_id: _, + message, + }) => { + assert_eq!(propagation_source, peers[0]); + assert_eq!(message.data.len(), max_size + 1); + } + ToSwarm::NotifyHandler { peer_id, .. } => { + assert_eq!(peer_id, peers[0]); + } + _ => panic!("Unexpected event"), + } + + // Simulate a peer sending a message exceeding the topic-specific max_transmit_size (2048 + // bytes). The codec's max_length is set high to allow encoding/decoding the RPC, while + // max_transmit_sizes enforces the custom topic limit. + let mut max_transmit_size_map = HashMap::new(); + max_transmit_size_map.insert(topic_hash, max_size); + + let mut codec = GossipsubCodec::new( + Config::default_max_transmit_size() * 2, + ValidationMode::None, + max_transmit_size_map, + ); + let mut buf = BytesMut::new(); + let rpc = proto::RPC { + publish: vec![proto::Message { + from: Some(peers[0].to_bytes()), + data: Some(vec![0u8; max_size + 1]), + seqno: Some(1u64.to_be_bytes().to_vec()), + topic: topic_hashes[0].to_string(), + signature: None, + key: None, + }], + subscriptions: vec![], + control: None, + }; + codec.encode(rpc, &mut buf).unwrap(); + + let decoded = codec.decode(&mut buf).unwrap().unwrap(); + match decoded { + HandlerEvent::Message { + rpc, + invalid_messages, + } => { + assert!( + rpc.messages.is_empty(), + "No valid messages should be present" + ); + assert_eq!(invalid_messages.len(), 1, "One message should be invalid"); + let (invalid_msg, error) = &invalid_messages[0]; + assert_eq!(invalid_msg.data.len(), max_size + 1); + assert_eq!(error, &ValidationError::MessageSizeTooLargeForTopic); + } + _ => panic!("Unexpected event"), + } +} + +#[test] +fn test_validation_message_size_within_topic_specific() { + let topic = Topic::new("test"); + let topic_hash = topic.hash(); + let max_size = 2048; + + let config = ConfigBuilder::default() + .max_transmit_size_for_topic(max_size, topic_hash.clone()) + .validation_mode(ValidationMode::None) + .build() + .unwrap(); + + let (mut gs, peers, _, topic_hashes) = DefaultBehaviourTestBuilder::default() + .peer_no(1) + .topics(vec![String::from("test")]) + .to_subscribe(true) + .gs_config(config) + .create_network(); + + let data = vec![0u8; max_size - 100]; + let raw_message = RawMessage { + source: Some(peers[0]), + data, + sequence_number: Some(1u64), + topic: topic_hashes[0].clone(), + signature: None, + key: None, + validated: false, + }; + + gs.on_connection_handler_event( + peers[0], + ConnectionId::new_unchecked(0), + HandlerEvent::Message { + rpc: RpcIn { + messages: vec![raw_message], + subscriptions: vec![], + control_msgs: vec![], + }, + invalid_messages: vec![], + }, + ); + + let event = gs.events.pop_front().expect("Event should be generated"); + match event { + ToSwarm::GenerateEvent(Event::Message { + propagation_source, + message_id: _, + message, + }) => { + assert_eq!(propagation_source, peers[0]); + assert_eq!(message.data.len(), max_size - 100); + } + ToSwarm::NotifyHandler { peer_id, .. } => { + assert_eq!(peer_id, peers[0]); + } + _ => panic!("Unexpected event"), + } + + // Simulate a peer sending a message within the topic-specific max_transmit_size (2048 bytes). + // The codec's max_length allows encoding/decoding the RPC, and max_transmit_sizes confirms + // the message size is acceptable for the topic. + let mut max_transmit_size_map = HashMap::new(); + max_transmit_size_map.insert(topic_hash, max_size); + + let mut codec = GossipsubCodec::new( + Config::default_max_transmit_size() * 2, + ValidationMode::None, + max_transmit_size_map, + ); + let mut buf = BytesMut::new(); + let rpc = proto::RPC { + publish: vec![proto::Message { + from: Some(peers[0].to_bytes()), + data: Some(vec![0u8; max_size - 100]), + seqno: Some(1u64.to_be_bytes().to_vec()), + topic: topic_hashes[0].to_string(), + signature: None, + key: None, + }], + subscriptions: vec![], + control: None, + }; + codec.encode(rpc, &mut buf).unwrap(); + + let decoded = codec.decode(&mut buf).unwrap().unwrap(); + match decoded { + HandlerEvent::Message { + rpc, + invalid_messages, + } => { + assert_eq!(rpc.messages.len(), 1, "One valid message should be present"); + assert!(invalid_messages.is_empty(), "No messages should be invalid"); + assert_eq!(rpc.messages[0].data.len(), max_size - 100); + } + _ => panic!("Unexpected event"), + } +} diff --git a/protocols/gossipsub/src/config.rs b/protocols/gossipsub/src/config.rs index 366f88f63bf..615bd08da30 100644 --- a/protocols/gossipsub/src/config.rs +++ b/protocols/gossipsub/src/config.rs @@ -127,7 +127,6 @@ pub struct Config { max_ihave_length: usize, max_ihave_messages: usize, iwant_followup_time: Duration, - published_message_ids_cache_time: Duration, connection_handler_queue_len: usize, connection_handler_publish_duration: Duration, connection_handler_forward_duration: Duration, @@ -444,11 +443,6 @@ impl Config { self.protocol.protocol_ids.contains(&FLOODSUB_PROTOCOL) } - /// Published message ids time cache duration. The default is 10 seconds. - pub fn published_message_ids_cache_time(&self) -> Duration { - self.published_message_ids_cache_time - } - /// The max number of messages a `ConnectionHandler` can buffer. The default is 5000. pub fn connection_handler_queue_len(&self) -> usize { self.connection_handler_queue_len @@ -546,7 +540,6 @@ impl Default for ConfigBuilder { max_ihave_length: 5000, max_ihave_messages: 10, iwant_followup_time: Duration::from_secs(3), - published_message_ids_cache_time: Duration::from_secs(10), connection_handler_queue_len: 5000, connection_handler_publish_duration: Duration::from_secs(5), connection_handler_forward_duration: Duration::from_secs(1), @@ -768,13 +761,22 @@ impl ConfigBuilder { self } - /// The maximum byte size for each gossip (default is 2048 bytes). + /// The maximum byte size for each gossip (default is 65536 bytes). + /// + /// ```rust + /// use libp2p_gossipsub::ConfigBuilder; + /// let mut config = ConfigBuilder::default(); + /// assert_eq!(config.build().unwrap().max_transmit_size(), 65536); + /// config.max_transmit_size(1 << 20); + /// assert_eq!(config.build().unwrap().max_transmit_size(), 1 << 20); + /// ``` pub fn max_transmit_size(&mut self, max_transmit_size: usize) -> &mut Self { self.config.protocol.default_max_transmit_size = max_transmit_size; self } - /// The maximum byte size for each gossip for a given topic. (default is 2048 bytes). + /// The maximum byte size for each gossip for a given topic. (default is + /// [`Self::max_transmit_size`]). pub fn max_transmit_size_for_topic( &mut self, max_transmit_size: usize, @@ -865,8 +867,8 @@ impl ConfigBuilder { /// This is how long to wait before resubscribing to the topic. A short backoff period in case /// of an unsubscribe event allows reaching a healthy mesh in a more timely manner. The default /// is 10 seconds. - pub fn unsubscribe_backoff(&mut self, unsubscribe_backoff: u64) -> &mut Self { - self.config.unsubscribe_backoff = Duration::from_secs(unsubscribe_backoff); + pub fn unsubscribe_backoff(&mut self, unsubscribe_backoff: Duration) -> &mut Self { + self.config.unsubscribe_backoff = unsubscribe_backoff; self } @@ -1008,15 +1010,6 @@ impl ConfigBuilder { self } - /// Published message ids time cache duration. The default is 10 seconds. - pub fn published_message_ids_cache_time( - &mut self, - published_message_ids_cache_time: Duration, - ) -> &mut Self { - self.config.published_message_ids_cache_time = published_message_ids_cache_time; - self - } - /// The max number of messages a `ConnectionHandler` can buffer. The default is 5000. pub fn connection_handler_queue_len(&mut self, len: usize) -> &mut Self { self.config.connection_handler_queue_len = len; @@ -1082,15 +1075,6 @@ impl ConfigBuilder { self } - /// The topic max size sets message sizes for a given topic. - pub fn set_topic_max_transmit_size(&mut self, topic: TopicHash, max_size: usize) -> &mut Self { - self.config - .protocol - .max_transmit_sizes - .insert(topic, max_size); - self - } - /// Constructs a [`Config`] from the given configuration and validates the settings. pub fn build(&self) -> Result { // check all constraints on config @@ -1177,10 +1161,6 @@ impl std::fmt::Debug for Config { let _ = builder.field("max_ihave_length", &self.max_ihave_length); let _ = builder.field("max_ihave_messages", &self.max_ihave_messages); let _ = builder.field("iwant_followup_time", &self.iwant_followup_time); - let _ = builder.field( - "published_message_ids_cache_time", - &self.published_message_ids_cache_time, - ); let _ = builder.field( "idontwant_message_size_threshold", &self.idontwant_message_size_threshold, diff --git a/protocols/gossipsub/src/handler.rs b/protocols/gossipsub/src/handler.rs index 99d62de953f..8e2066e365f 100644 --- a/protocols/gossipsub/src/handler.rs +++ b/protocols/gossipsub/src/handler.rs @@ -25,7 +25,7 @@ use std::{ use asynchronous_codec::Framed; use futures::{future::Either, prelude::*, StreamExt}; -use libp2p_core::upgrade::DeniedUpgrade; +use libp2p_core::{upgrade::DeniedUpgrade, PeerId}; use libp2p_swarm::{ handler::{ ConnectionEvent, ConnectionHandler, ConnectionHandlerEvent, DialUpgradeError, @@ -37,9 +37,9 @@ use web_time::Instant; use crate::{ protocol::{GossipsubCodec, ProtocolConfig}, - rpc::Receiver, + queue::Queue, rpc_proto::proto, - types::{PeerKind, RawMessage, Rpc, RpcOut}, + types::{PeerKind, RawMessage, RpcIn, RpcOut}, ValidationError, }; @@ -51,7 +51,7 @@ pub enum HandlerEvent { /// any) that were received. Message { /// The GossipsubRPC message excluding any invalid messages. - rpc: Rpc, + rpc: RpcIn, /// Any invalid messages that were received in the RPC, along with the associated /// validation error. invalid_messages: Vec<(RawMessage, ValidationError)>, @@ -89,6 +89,9 @@ pub enum Handler { /// Protocol Handler that manages a single long-lived substream with a peer. pub struct EnabledHandler { + /// Remote `PeerId` for this `ConnectionHandler`. + peer_id: PeerId, + /// Upgrade configuration for the gossipsub protocol. listen_protocol: ProtocolConfig, @@ -98,8 +101,8 @@ pub struct EnabledHandler { /// The single long-lived inbound substream. inbound_substream: Option, - /// Queue of values that we want to send to the remote - send_queue: Receiver, + /// Queue of dispatched Rpc messages to send. + message_queue: Queue, /// Flag indicating that an outbound substream is being established to prevent duplicate /// requests. @@ -162,15 +165,20 @@ enum OutboundSubstreamState { impl Handler { /// Builds a new [`Handler`]. - pub fn new(protocol_config: ProtocolConfig, message_queue: Receiver) -> Self { + pub(crate) fn new( + peer_id: PeerId, + protocol_config: ProtocolConfig, + message_queue: Queue, + ) -> Self { Handler::Enabled(EnabledHandler { + peer_id, listen_protocol: protocol_config, inbound_substream: None, outbound_substream: None, outbound_substream_establishing: false, outbound_substream_attempts: 0, inbound_substream_attempts: 0, - send_queue: message_queue, + message_queue, peer_kind: None, peer_kind_sent: false, last_io_activity: Instant::now(), @@ -234,7 +242,7 @@ impl EnabledHandler { } // determine if we need to create the outbound stream - if !self.send_queue.poll_is_empty(cx) + if !self.message_queue.is_empty() && self.outbound_substream.is_none() && !self.outbound_substream_establishing { @@ -252,15 +260,19 @@ impl EnabledHandler { { // outbound idle state Some(OutboundSubstreamState::WaitingOutput(substream)) => { - if let Poll::Ready(Some(mut message)) = self.send_queue.poll_next_unpin(cx) { + if let Poll::Ready(mut message) = Pin::new(&mut self.message_queue).poll_pop(cx) + { + tracing::debug!(peer=%self.peer_id, ?message, "Sending gossipsub message"); match message { RpcOut::Publish { message: _, ref mut timeout, + .. } | RpcOut::Forward { message: _, ref mut timeout, + .. } => { if Pin::new(timeout).poll(cx).is_ready() { // Inform the behaviour and end the poll. @@ -407,13 +419,6 @@ impl EnabledHandler { } } - // Drop the next message in queue if it's stale. - if let Poll::Ready(Some(rpc)) = self.send_queue.poll_stale(cx) { - return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( - HandlerEvent::MessageDropped(rpc), - )); - } - Poll::Pending } } diff --git a/protocols/gossipsub/src/lib.rs b/protocols/gossipsub/src/lib.rs index ae614367e14..f1d42d6cddb 100644 --- a/protocols/gossipsub/src/lib.rs +++ b/protocols/gossipsub/src/lib.rs @@ -105,7 +105,7 @@ mod mcache; mod metrics; mod peer_score; mod protocol; -mod rpc; +mod queue; mod rpc_proto; mod subscription_filter; mod time_cache; @@ -134,8 +134,5 @@ pub use self::{ types::{FailedMessages, Message, MessageAcceptance, MessageId, RawMessage}, }; -#[deprecated(note = "Will be removed from the public API.")] -pub type Rpc = self::types::Rpc; - pub type IdentTopic = Topic; pub type Sha256Topic = Topic; diff --git a/protocols/gossipsub/src/mcache.rs b/protocols/gossipsub/src/mcache.rs index 8ed71ea07f2..b1d6084f3ae 100644 --- a/protocols/gossipsub/src/mcache.rs +++ b/protocols/gossipsub/src/mcache.rs @@ -76,6 +76,9 @@ impl MessageCache { /// /// Returns true if the message didn't already exist in the cache. pub(crate) fn put(&mut self, message_id: &MessageId, msg: RawMessage) -> bool { + if self.history.is_empty() { + return true; + } match self.msgs.entry(message_id.clone()) { Entry::Occupied(_) => { // Don't add duplicate entries to the cache. @@ -187,6 +190,10 @@ impl MessageCache { /// Shift the history array down one and delete messages associated with the /// last entry. pub(crate) fn shift(&mut self) { + if self.history.is_empty() { + return; + } + for entry in self.history.pop().expect("history is always > 1") { if let Some((msg, _)) = self.msgs.remove(&entry.mid) { if !msg.validated { diff --git a/protocols/gossipsub/src/metrics.rs b/protocols/gossipsub/src/metrics.rs index 37fe5481689..1394d9a92a7 100644 --- a/protocols/gossipsub/src/metrics.rs +++ b/protocols/gossipsub/src/metrics.rs @@ -133,12 +133,6 @@ pub(crate) struct Metrics { ignored_messages: Family, /// The number of messages rejected by the application (validation result). rejected_messages: Family, - /// The number of publish messages dropped by the sender. - publish_messages_dropped: Family, - /// The number of forward messages dropped by the sender. - forward_messages_dropped: Family, - /// The number of messages that timed out and could not be sent. - timedout_messages_dropped: Family, // Metrics regarding mesh state /// Number of peers in our mesh. This metric should be updated with the count of peers for a @@ -193,10 +187,15 @@ pub(crate) struct Metrics { /// The number of msg_id's we have received in every IDONTWANT control message. idontwant_msgs_ids: Counter, - /// The size of the priority queue. - priority_queue_size: Histogram, - /// The size of the non-priority queue. - non_priority_queue_size: Histogram, + /// The size of the queue by priority. + queue_size: Family, + + /// Failed messages by message type. + failed_messages: Family, + + /// The number of messages we have removed from a queue that we would otherwise send. A rough + /// guide to measure of bandwidth saved. + removed_queued_messages: Counter, } impl Metrics { @@ -245,21 +244,6 @@ impl Metrics { "Number of rejected messages received for each topic" ); - let publish_messages_dropped = register_family!( - "publish_messages_dropped_per_topic", - "Number of publish messages dropped per topic" - ); - - let forward_messages_dropped = register_family!( - "forward_messages_dropped_per_topic", - "Number of forward messages dropped per topic" - ); - - let timedout_messages_dropped = register_family!( - "timedout_messages_dropped_per_topic", - "Number of timedout messages dropped per topic" - ); - let mesh_peer_counts = register_family!( "mesh_peer_counts", "Number of peers in each topic in our mesh" @@ -361,20 +345,36 @@ impl Metrics { metric }; - let priority_queue_size = Histogram::new(linear_buckets(0.0, 25.0, 100)); + let queue_size = Family::::new_with_constructor(|| { + Histogram::new(linear_buckets(0.0, 50.0, 100)) + }); registry.register( - "priority_queue_size", - "Histogram of observed priority queue sizes", - priority_queue_size.clone(), + "queue_size", + "Histogram of observed queue sizes", + queue_size.clone(), ); - let non_priority_queue_size = Histogram::new(linear_buckets(0.0, 25.0, 100)); + let failed_messages = Family::::new_with_constructor(|| { + Histogram::new([ + 0.0, 1.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 1000.0, 2000.0, + ]) + }); registry.register( - "non_priority_queue_size", - "Histogram of observed non-priority queue sizes", - non_priority_queue_size.clone(), + "failed_messages", + "Histogram of observed failed messages by type", + failed_messages.clone(), ); + let removed_queued_messages = { + let metric = Counter::default(); + registry.register( + "removed_queued_messages", + "Number of messages we have removed from all our queues due to IDONTWANTs", + metric.clone(), + ); + metric + }; + Self { max_topics, max_never_subscribed_topics, @@ -385,9 +385,6 @@ impl Metrics { accepted_messages, ignored_messages, rejected_messages, - publish_messages_dropped, - forward_messages_dropped, - timedout_messages_dropped, mesh_peer_counts, mesh_peer_inclusion_events, mesh_peer_churn_events, @@ -405,8 +402,9 @@ impl Metrics { topic_iwant_msgs, idontwant_msgs, idontwant_msgs_ids, - priority_queue_size, - non_priority_queue_size, + queue_size, + failed_messages, + removed_queued_messages, } } @@ -537,27 +535,6 @@ impl Metrics { } } - /// Register dropping a Publish message over a topic. - pub(crate) fn publish_msg_dropped(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.publish_messages_dropped.get_or_create(topic).inc(); - } - } - - /// Register dropping a Forward message over a topic. - pub(crate) fn forward_msg_dropped(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.forward_messages_dropped.get_or_create(topic).inc(); - } - } - - /// Register dropping a message that timedout over a topic. - pub(crate) fn timeout_msg_dropped(&mut self, topic: &TopicHash) { - if self.register_topic(topic).is_ok() { - self.timedout_messages_dropped.get_or_create(topic).inc(); - } - } - /// Register that a message was received (and was not a duplicate). pub(crate) fn msg_recvd(&mut self, topic: &TopicHash) { if self.register_topic(topic).is_ok() { @@ -616,12 +593,20 @@ impl Metrics { /// Observes a priority queue size. pub(crate) fn observe_priority_queue_size(&mut self, len: usize) { - self.priority_queue_size.observe(len as f64); + self.queue_size + .get_or_create(&MessageTypeLabel { + message_type: MessageType::Priority, + }) + .observe(len as f64); } /// Observes a non-priority queue size. pub(crate) fn observe_non_priority_queue_size(&mut self, len: usize) { - self.non_priority_queue_size.observe(len as f64); + self.queue_size + .get_or_create(&MessageTypeLabel { + message_type: MessageType::NonPriority, + }) + .observe(len as f64); } /// Observe a score of a mesh peer. @@ -655,6 +640,30 @@ impl Metrics { self.topic_info.insert(topic_hash, true); } } + + /// Observe the failed priority messages. + pub(crate) fn observe_failed_priority_messages(&mut self, messages: usize) { + self.failed_messages + .get_or_create(&MessageTypeLabel { + message_type: MessageType::Priority, + }) + .observe(messages as f64); + } + + /// Observe the failed non priority messages. + pub(crate) fn observe_failed_non_priority_messages(&mut self, messages: usize) { + self.failed_messages + .get_or_create(&MessageTypeLabel { + message_type: MessageType::NonPriority, + }) + .observe(messages as f64); + } + + /// Register the number of removed messages from the `Handler` queue + /// When receiving IDONTWANT messages + pub(crate) fn register_removed_messages(&mut self, removed_messages: usize) { + self.removed_queued_messages.inc_by(removed_messages as u64); + } } /// Reasons why a peer was included in the mesh. @@ -698,6 +707,15 @@ pub(crate) enum Penalty { IPColocation, } +/// Kinds of messages in the send queue. +#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, EncodeLabelValue)] +pub(crate) enum MessageType { + /// A priority message. + Priority, + /// Non Priority Message. + NonPriority, +} + /// Label for the mesh inclusion event metrics. #[derive(PartialEq, Eq, Hash, EncodeLabelSet, Clone, Debug)] struct InclusionLabel { @@ -724,6 +742,12 @@ struct PenaltyLabel { penalty: Penalty, } +/// Label for the queue message priority kind. +#[derive(PartialEq, Eq, Hash, EncodeLabelSet, Clone, Debug)] +struct MessageTypeLabel { + message_type: MessageType, +} + #[derive(Clone)] struct HistBuilder { buckets: Vec, diff --git a/protocols/gossipsub/src/peer_score.rs b/protocols/gossipsub/src/peer_score.rs index 1e1c099fd27..7a30038c48b 100644 --- a/protocols/gossipsub/src/peer_score.rs +++ b/protocols/gossipsub/src/peer_score.rs @@ -311,11 +311,14 @@ impl PeerScore { if topic_stats.mesh_message_deliveries_active && topic_stats.mesh_message_deliveries < topic_params.mesh_message_deliveries_threshold + && topic_params.mesh_message_deliveries_weight != 0.0 { let deficit = topic_params.mesh_message_deliveries_threshold - topic_stats.mesh_message_deliveries; let p3 = deficit * deficit; - topic_score += p3 * topic_params.mesh_message_deliveries_weight; + let penalty = p3 * topic_params.mesh_message_deliveries_weight; + + topic_score += penalty; #[cfg(feature = "metrics")] report .penalties @@ -324,7 +327,7 @@ impl PeerScore { peer=%peer_id, %topic, %deficit, - penalty=%topic_score, + penalty=%penalty, "[Penalty] The peer has a mesh deliveries deficit and will be penalized" ); } @@ -367,7 +370,9 @@ impl PeerScore { // addr. It is quadratic, and the weight is negative (validated by // peer_score_params.validate()). if let Some(peers_in_ip) = self.peer_ips.get(ip).map(|peers| peers.len()) { - if (peers_in_ip as f64) > self.params.ip_colocation_factor_threshold { + if (peers_in_ip as f64) > self.params.ip_colocation_factor_threshold + && self.params.ip_colocation_factor_weight != 0.0 + { let surplus = (peers_in_ip as f64) - self.params.ip_colocation_factor_threshold; let p6 = surplus * surplus; #[cfg(feature = "metrics")] diff --git a/protocols/gossipsub/src/protocol.rs b/protocols/gossipsub/src/protocol.rs index f77f699539f..74dcc669f55 100644 --- a/protocols/gossipsub/src/protocol.rs +++ b/protocols/gossipsub/src/protocol.rs @@ -36,7 +36,7 @@ use crate::{ topic::TopicHash, types::{ ControlAction, Graft, IDontWant, IHave, IWant, MessageId, PeerInfo, PeerKind, Prune, - RawMessage, Rpc, Subscription, SubscriptionAction, + RawMessage, RpcIn, Subscription, SubscriptionAction, }, ValidationError, }; @@ -564,7 +564,7 @@ impl Decoder for GossipsubCodec { } Ok(Some(HandlerEvent::Message { - rpc: Rpc { + rpc: RpcIn { messages, subscriptions: rpc .subscriptions @@ -587,12 +587,16 @@ impl Decoder for GossipsubCodec { #[cfg(test)] mod tests { + use std::time::Duration; + + use futures_timer::Delay; use libp2p_identity::Keypair; use quickcheck::*; use super::*; use crate::{ - config::Config, Behaviour, ConfigBuilder, IdentTopic as Topic, MessageAuthenticity, Version, + config::Config, types::RpcOut, Behaviour, ConfigBuilder, IdentTopic as Topic, + MessageAuthenticity, Version, }; #[derive(Clone, Debug)] @@ -652,10 +656,10 @@ mod tests { fn prop(message: Message) { let message = message.0; - let rpc = Rpc { - messages: vec![message.clone()], - subscriptions: vec![], - control_msgs: vec![], + let rpc = RpcOut::Publish { + message: message.clone(), + timeout: Delay::new(Duration::from_secs(1)), + message_id: MessageId(vec![0, 0]), }; let mut codec = diff --git a/protocols/gossipsub/src/queue.rs b/protocols/gossipsub/src/queue.rs new file mode 100644 index 00000000000..971ae801f83 --- /dev/null +++ b/protocols/gossipsub/src/queue.rs @@ -0,0 +1,298 @@ +// Copyright 2020 Sigma Prime Pty Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use std::{ + collections::{HashMap, VecDeque}, + pin::Pin, + sync::{atomic::AtomicUsize, Arc, Mutex}, + task::{Context, Poll, Waker}, +}; + +use crate::{types::RpcOut, MessageId}; + +const CONTROL_MSGS_LIMIT: usize = 20_000; + +/// An async priority queue used to dispatch messages from the `NetworkBehaviour` +/// Provides a clean abstraction over high-priority (unbounded), control (bounded), +/// and non priority (bounded) message queues. +#[derive(Debug)] +pub(crate) struct Queue { + /// High-priority unbounded queue (Subscribe, Unsubscribe) + pub(crate) priority: Shared, + /// Control messages bounded queue (Graft, Prune, IDontWant) + pub(crate) control: Shared, + /// Low-priority bounded queue (Publish, Forward, IHave, IWant) + pub(crate) non_priority: Shared, + /// The id of the current reference of the counter. + pub(crate) id: usize, + /// The total number of references for the queue. + pub(crate) count: Arc, +} + +impl Queue { + /// Create a new `Queue` with `capacity`. + pub(crate) fn new(capacity: usize) -> Self { + Self { + priority: Shared::new(), + control: Shared::with_capacity(CONTROL_MSGS_LIMIT), + non_priority: Shared::with_capacity(capacity), + id: 1, + count: Arc::new(AtomicUsize::new(1)), + } + } + + /// Try to push a message to the Queue, return Err if the queue is full, + /// which will only happen for control and non priority messages. + pub(crate) fn try_push(&mut self, message: RpcOut) -> Result<(), Box> { + match message { + RpcOut::Subscribe(_) | RpcOut::Unsubscribe(_) => { + self.priority + .try_push(message) + .expect("Shared is unbounded"); + Ok(()) + } + RpcOut::Graft(_) | RpcOut::Prune(_) | RpcOut::IDontWant(_) => { + self.control.try_push(message) + } + RpcOut::Publish { .. } + | RpcOut::Forward { .. } + | RpcOut::IHave(_) + | RpcOut::IWant(_) => self.non_priority.try_push(message), + } + } + + /// Remove pending low priority Publish and Forward messages. + /// Returns the number of messages removed. + pub(crate) fn remove_data_messages(&mut self, message_ids: &[MessageId]) -> usize { + if message_ids.is_empty() { + return 0; + } + + let mut count = 0; + self.non_priority.retain(|message| match message { + RpcOut::Publish { message_id, .. } | RpcOut::Forward { message_id, .. } => { + if message_ids.contains(message_id) { + count += 1; + false + } else { + true + } + } + _ => true, + }); + count + } + + /// Pop an element from the queue. + pub(crate) fn poll_pop(&mut self, cx: &mut Context) -> Poll { + // First we try the priority messages. + if let Poll::Ready(rpc) = Pin::new(&mut self.priority).poll_pop(cx) { + return Poll::Ready(rpc); + } + + // Then we try the control messages. + if let Poll::Ready(rpc) = Pin::new(&mut self.control).poll_pop(cx) { + return Poll::Ready(rpc); + } + + // Finally we try the non priority messages + if let Poll::Ready(rpc) = Pin::new(&mut self.non_priority).poll_pop(cx) { + return Poll::Ready(rpc); + } + + Poll::Pending + } + + /// Check if the queue is empty. + pub(crate) fn is_empty(&self) -> bool { + if !self.priority.is_empty() { + return false; + } + + if !self.control.is_empty() { + return false; + } + + if !self.non_priority.is_empty() { + return false; + } + + true + } + + /// Returns the length of the priority queue. + #[cfg(feature = "metrics")] + pub(crate) fn priority_len(&self) -> usize { + self.priority.len() + self.control.len() + } + + /// Returns the length of the non priority queue. + #[cfg(feature = "metrics")] + pub(crate) fn non_priority_len(&self) -> usize { + self.non_priority.len() + } + + /// Attempts to pop a message from the queue. + /// returns None if the queue is empty. + #[cfg(test)] + pub(crate) fn try_pop(&mut self) -> Option { + // Try priority first + self.priority + .try_pop() + // Then control messages + .or_else(|| self.control.try_pop()) + // Finally non priority + .or_else(|| self.non_priority.try_pop()) + } +} + +impl Clone for Queue { + fn clone(&self) -> Self { + let new_id = self.count.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Self { + priority: Shared { + inner: self.priority.inner.clone(), + capacity: self.priority.capacity, + id: new_id, + }, + control: Shared { + inner: self.control.inner.clone(), + capacity: self.control.capacity, + id: new_id, + }, + non_priority: Shared { + inner: self.non_priority.inner.clone(), + capacity: self.non_priority.capacity, + id: new_id, + }, + id: self.id, + count: self.count.clone(), + } + } +} + +/// The internal shared part of the queue, +/// that allows for shallow copies of the queue among each connection of the remote. +#[derive(Debug)] +pub(crate) struct Shared { + inner: Arc>, + capacity: Option, + id: usize, +} + +impl Shared { + pub(crate) fn with_capacity(capacity: usize) -> Self { + Self { + inner: Arc::new(Mutex::new(SharedInner { + queue: VecDeque::new(), + pending_pops: Default::default(), + })), + capacity: Some(capacity), + id: 1, + } + } + + pub(crate) fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(SharedInner { + queue: VecDeque::new(), + pending_pops: Default::default(), + })), + capacity: None, + id: 1, + } + } + + /// Pop an element from the queue. + pub(crate) fn poll_pop(self: std::pin::Pin<&mut Self>, cx: &mut Context) -> Poll { + let mut guard = self.inner.lock().expect("lock to not be poisoned"); + match guard.queue.pop_front() { + Some(t) => Poll::Ready(t), + None => { + guard + .pending_pops + .entry(self.id) + .or_insert(cx.waker().clone()); + Poll::Pending + } + } + } + + pub(crate) fn try_push(&mut self, message: RpcOut) -> Result<(), Box> { + let mut guard = self.inner.lock().expect("lock to not be poisoned"); + if self + .capacity + .is_some_and(|capacity| guard.queue.len() >= capacity) + { + return Err(Box::new(message)); + } + + guard.queue.push_back(message); + // Wake pending registered pops. + for (_, s) in guard.pending_pops.drain() { + s.wake(); + } + + Ok(()) + } + + /// Retain only the elements specified by the predicate. + /// In other words, remove all elements e for which f(&e) returns false. The elements are + /// visited in unsorted (and unspecified) order. Returns the cleared messages. + pub(crate) fn retain bool>(&mut self, f: F) { + let mut shared = self.inner.lock().expect("lock to not be poisoned"); + shared.queue.retain(f); + } + + /// Check if the queue is empty. + pub(crate) fn is_empty(&self) -> bool { + let guard = self.inner.lock().expect("lock to not be poisoned"); + guard.queue.len() == 0 + } + + /// Returns the length of the queue. + #[cfg(feature = "metrics")] + pub(crate) fn len(&self) -> usize { + let guard = self.inner.lock().expect("lock to not be poisoned"); + guard.queue.len() + } + + /// Attempts to pop an message from the queue. + /// returns None if the queue is empty. + #[cfg(test)] + pub(crate) fn try_pop(&mut self) -> Option { + let mut guard = self.inner.lock().expect("lock to not be poisoned"); + guard.queue.pop_front() + } +} + +impl Drop for Shared { + fn drop(&mut self) { + let mut guard = self.inner.lock().expect("lock to not be poisoned"); + guard.pending_pops.remove(&self.id); + } +} + +/// The shared stated by the `NetworkBehaviour`s and the `ConnectionHandler`s. +#[derive(Debug)] +struct SharedInner { + queue: VecDeque, + pending_pops: HashMap, +} diff --git a/protocols/gossipsub/src/rpc_proto.rs b/protocols/gossipsub/src/rpc_proto.rs index 2f6832a01a1..d85fe44133c 100644 --- a/protocols/gossipsub/src/rpc_proto.rs +++ b/protocols/gossipsub/src/rpc_proto.rs @@ -19,7 +19,7 @@ // DEALINGS IN THE SOFTWARE. pub(crate) mod proto { - #![allow(unreachable_pub)] + #![allow(unreachable_pub, dead_code)] include!("generated/mod.rs"); pub use self::gossipsub::pb::{mod_RPC::SubOpts, *}; } diff --git a/protocols/gossipsub/src/time_cache.rs b/protocols/gossipsub/src/time_cache.rs index ace02606e88..810f019a910 100644 --- a/protocols/gossipsub/src/time_cache.rs +++ b/protocols/gossipsub/src/time_cache.rs @@ -135,7 +135,7 @@ where } } - pub(crate) fn entry(&mut self, key: Key) -> Entry { + pub(crate) fn entry(&mut self, key: Key) -> Entry<'_, Key, Value> { let now = Instant::now(); self.remove_expired_keys(now); match self.map.entry(key) { diff --git a/protocols/gossipsub/src/types.rs b/protocols/gossipsub/src/types.rs index 4af8e4666da..bea0786e060 100644 --- a/protocols/gossipsub/src/types.rs +++ b/protocols/gossipsub/src/types.rs @@ -19,7 +19,10 @@ // DEALINGS IN THE SOFTWARE. //! A collection of types using the Gossipsub system. -use std::{collections::BTreeSet, fmt, fmt::Debug}; +use std::{ + collections::BTreeSet, + fmt::{self, Debug}, +}; use futures_timer::Delay; use hashlink::LinkedHashMap; @@ -30,34 +33,17 @@ use quick_protobuf::MessageWrite; use serde::{Deserialize, Serialize}; use web_time::Instant; -use crate::{rpc::Sender, rpc_proto::proto, TopicHash}; +use crate::{queue::Queue, rpc_proto::proto, TopicHash}; /// Messages that have expired while attempting to be sent to a peer. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct FailedMessages { - /// The number of publish messages that failed to be published in a heartbeat. - pub publish: usize, - /// The number of forward messages that failed to be published in a heartbeat. - pub forward: usize, - /// The number of messages that were failed to be sent to the priority queue as it was full. + /// The number of messages that were failed to be sent to the priority queue as it was + /// full. pub priority: usize, - /// The number of messages that were failed to be sent to the non-priority queue as it was + /// The number of messages that were failed to be sent to the non priority queue as it was /// full. pub non_priority: usize, - /// The number of messages that timed out and could not be sent. - pub timeout: usize, -} - -impl FailedMessages { - /// The total number of messages that failed due to the queue being full. - pub fn total_queue_full(&self) -> usize { - self.priority + self.non_priority - } - - /// The total failed messages in a heartbeat. - pub fn total(&self) -> usize { - self.priority + self.non_priority - } } #[derive(Debug)] @@ -111,10 +97,10 @@ pub(crate) struct PeerDetails { pub(crate) connections: Vec, /// Subscribed topics. pub(crate) topics: BTreeSet, - /// The rpc sender to the connection handler(s). - pub(crate) sender: Sender, /// Don't send messages. pub(crate) dont_send: LinkedHashMap, + /// Message queue consumed by the connection handler. + pub(crate) messages: Queue, } /// Describes the types of peers that can exist in the gossipsub context. @@ -319,10 +305,18 @@ pub struct IDontWant { pub enum RpcOut { /// Publish a Gossipsub message on network.`timeout` limits the duration the message /// can wait to be sent before it is abandoned. - Publish { message: RawMessage, timeout: Delay }, + Publish { + message_id: MessageId, + message: RawMessage, + timeout: Delay, + }, /// Forward a Gossipsub message on network. `timeout` limits the duration the message /// can wait to be sent before it is abandoned. - Forward { message: RawMessage, timeout: Delay }, + Forward { + message_id: MessageId, + message: RawMessage, + timeout: Delay, + }, /// Subscribe a topic. Subscribe(TopicHash), /// Unsubscribe a topic. @@ -346,24 +340,30 @@ impl RpcOut { pub fn into_protobuf(self) -> proto::RPC { self.into() } + + /// Returns true if the `RpcOut` is priority. + pub(crate) fn priority(&self) -> bool { + matches!( + self, + RpcOut::Subscribe(_) + | RpcOut::Unsubscribe(_) + | RpcOut::Graft(_) + | RpcOut::Prune(_) + | RpcOut::IDontWant(_) + ) + } } impl From for proto::RPC { /// Converts the RPC into protobuf format. fn from(rpc: RpcOut) -> Self { match rpc { - RpcOut::Publish { - message, - timeout: _, - } => proto::RPC { + RpcOut::Publish { message, .. } => proto::RPC { subscriptions: Vec::new(), publish: vec![message.into()], control: None, }, - RpcOut::Forward { - message, - timeout: _, - } => proto::RPC { + RpcOut::Forward { message, .. } => proto::RPC { publish: vec![message.into()], subscriptions: Vec::new(), control: None, @@ -472,9 +472,9 @@ impl From for proto::RPC { } } -/// An RPC received/sent. +/// A Gossipsub RPC message received. #[derive(Clone, PartialEq, Eq, Hash)] -pub struct Rpc { +pub struct RpcIn { /// List of messages that were part of this RPC query. pub messages: Vec, /// List of subscriptions. @@ -483,120 +483,7 @@ pub struct Rpc { pub control_msgs: Vec, } -impl Rpc { - /// Converts the GossipsubRPC into its protobuf format. - // A convenience function to avoid explicitly specifying types. - pub fn into_protobuf(self) -> proto::RPC { - self.into() - } -} - -impl From for proto::RPC { - /// Converts the RPC into protobuf format. - fn from(rpc: Rpc) -> Self { - // Messages - let mut publish = Vec::new(); - - for message in rpc.messages.into_iter() { - let message = proto::Message { - from: message.source.map(|m| m.to_bytes()), - data: Some(message.data), - seqno: message.sequence_number.map(|s| s.to_be_bytes().to_vec()), - topic: TopicHash::into_string(message.topic), - signature: message.signature, - key: message.key, - }; - - publish.push(message); - } - - // subscriptions - let subscriptions = rpc - .subscriptions - .into_iter() - .map(|sub| proto::SubOpts { - subscribe: Some(sub.action == SubscriptionAction::Subscribe), - topic_id: Some(sub.topic_hash.into_string()), - }) - .collect::>(); - - // control messages - let mut control = proto::ControlMessage { - ihave: Vec::new(), - iwant: Vec::new(), - graft: Vec::new(), - prune: Vec::new(), - idontwant: Vec::new(), - }; - - let empty_control_msg = rpc.control_msgs.is_empty(); - - for action in rpc.control_msgs { - match action { - // collect all ihave messages - ControlAction::IHave(IHave { - topic_hash, - message_ids, - }) => { - let rpc_ihave = proto::ControlIHave { - topic_id: Some(topic_hash.into_string()), - message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), - }; - control.ihave.push(rpc_ihave); - } - ControlAction::IWant(IWant { message_ids }) => { - let rpc_iwant = proto::ControlIWant { - message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), - }; - control.iwant.push(rpc_iwant); - } - ControlAction::Graft(Graft { topic_hash }) => { - let rpc_graft = proto::ControlGraft { - topic_id: Some(topic_hash.into_string()), - }; - control.graft.push(rpc_graft); - } - ControlAction::Prune(Prune { - topic_hash, - peers, - backoff, - }) => { - let rpc_prune = proto::ControlPrune { - topic_id: Some(topic_hash.into_string()), - peers: peers - .into_iter() - .map(|info| proto::PeerInfo { - peer_id: info.peer_id.map(|id| id.to_bytes()), - // TODO, see https://github.com/libp2p/specs/pull/217 - signed_peer_record: None, - }) - .collect(), - backoff, - }; - control.prune.push(rpc_prune); - } - ControlAction::IDontWant(IDontWant { message_ids }) => { - let rpc_idontwant = proto::ControlIDontWant { - message_ids: message_ids.into_iter().map(|msg_id| msg_id.0).collect(), - }; - control.idontwant.push(rpc_idontwant); - } - } - } - - proto::RPC { - subscriptions, - publish, - control: if empty_control_msg { - None - } else { - Some(control) - }, - } - } -} - -impl fmt::Debug for Rpc { +impl fmt::Debug for RpcIn { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut b = f.debug_struct("GossipsubRpc"); if !self.messages.is_empty() { diff --git a/protocols/identify/CHANGELOG.md b/protocols/identify/CHANGELOG.md index b707d4e401b..85a5beb0209 100644 --- a/protocols/identify/CHANGELOG.md +++ b/protocols/identify/CHANGELOG.md @@ -5,6 +5,8 @@ - Fix `Identify::discovered_peers` to remove peers on `DialError::{WrongPeerId, LocalPeerId}` events. See [PR 5890](https://github.com/libp2p/rust-libp2p/pull/5890). + + ## 0.46.0 - Add `hide_listen_addrs` option to prevent leaking (local) listen addresses. diff --git a/protocols/identify/src/behaviour.rs b/protocols/identify/src/behaviour.rs index 3a0a0898394..a50e8e64cbd 100644 --- a/protocols/identify/src/behaviour.rs +++ b/protocols/identify/src/behaviour.rs @@ -76,13 +76,12 @@ fn is_tcp_addr(addr: &Multiaddr) -> bool { let mut iter = addr.iter(); - let first = match iter.next() { - None => return false, - Some(p) => p, + let Some(first) = iter.next() else { + return false; }; - let second = match iter.next() { - None => return false, - Some(p) => p, + + let Some(second) = iter.next() else { + return false; }; matches!(first, Ip4(_) | Ip6(_) | Dns(_) | Dns4(_) | Dns6(_)) && matches!(second, Tcp(_)) @@ -120,9 +119,9 @@ pub struct Config { /// Application-specific version of the protocol family used by the peer, /// e.g. `ipfs/1.0.0` or `polkadot/1.0.0`. protocol_version: String, - /// The key of the local node. Only the public key will be report on the wire. + /// The key of the local node. Only the public key will be report on the wire. /// The behaviour will send signed [`PeerRecord`](libp2p_core::PeerRecord) in - /// its identify message only when supplied with a keypair. + /// its identify message only when supplied with a keypair. local_key: Arc, /// Name and version of the local peer implementation, similar to the /// `User-Agent` header in the HTTP protocol. @@ -161,7 +160,7 @@ pub struct Config { impl Config { /// Creates a new configuration for the identify [`Behaviour`] that - /// advertises the given protocol version and public key. + /// advertises the given protocol version and public key. /// Use [`new_with_signed_peer_record`](Config::new_with_signed_peer_record) for /// `signedPeerRecord` support. pub fn new(protocol_version: String, local_public_key: PublicKey) -> Self { @@ -169,7 +168,7 @@ impl Config { } /// Creates a new configuration for the identify [`Behaviour`] that - /// advertises the given protocol version and public key. + /// advertises the given protocol version and public key. /// The private key will be used to sign [`PeerRecord`](libp2p_core::PeerRecord) /// for verifiable address advertisement. pub fn new_with_signed_peer_record(protocol_version: String, local_keypair: &Keypair) -> Self { @@ -532,9 +531,8 @@ impl NetworkBehaviour for Behaviour { _addresses: &[Multiaddr], _effective_role: Endpoint, ) -> Result, ConnectionDenied> { - let peer = match maybe_peer { - None => return Ok(vec![]), - Some(peer) => peer, + let Some(peer) = maybe_peer else { + return Ok(vec![]); }; Ok(self.discovered_peers.get(&peer)) diff --git a/protocols/identify/src/handler.rs b/protocols/identify/src/handler.rs index c2e31ae95f6..b77450a617d 100644 --- a/protocols/identify/src/handler.rs +++ b/protocols/identify/src/handler.rs @@ -430,6 +430,7 @@ impl ConnectionHandler for Handler { ConnectionEvent::DialUpgradeError(DialUpgradeError { error, .. }) => { self.events.push(ConnectionHandlerEvent::NotifyBehaviour( Event::IdentificationError( + #[allow(unused)] error.map_upgrade_err(|e| libp2p_core::util::unreachable(e.into_inner())), ), )); diff --git a/protocols/kad/CHANGELOG.md b/protocols/kad/CHANGELOG.md index 5cb94686f39..d50f7319af8 100644 --- a/protocols/kad/CHANGELOG.md +++ b/protocols/kad/CHANGELOG.md @@ -1,7 +1,21 @@ -## 0.47.1 +## 0.49.0 + +- Remove no longer constructed GetRecordError::QuorumFailed. + See [PR 6106](https://github.com/libp2p/rust-libp2p/pull/6106) + +## 0.48.1 + +- Implement `Copy` for `QueryStats` and `ProgressStep` + See [PR 6083](https://github.com/libp2p/rust-libp2p/pull/6083) + +## 0.48.0 - Configurable outbound_substreams_timeout. See [PR 6015](https://github.com/libp2p/rust-libp2p/pull/6015). +- Rename `outbound_substreams_timeout` to `substreams_timeout` for future-proofness. + See [PR 6076](https://github.com/libp2p/rust-libp2p/pull/6076). + + ## 0.47.0 diff --git a/protocols/kad/Cargo.toml b/protocols/kad/Cargo.toml index a437b8692d5..fd0c8e0eb75 100644 --- a/protocols/kad/Cargo.toml +++ b/protocols/kad/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-kad" edition.workspace = true rust-version = { workspace = true } description = "Kademlia protocol for libp2p" -version = "0.47.1" +version = "0.49.0" authors = ["Parity Technologies "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" @@ -37,7 +37,7 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } futures-timer = "3.0" libp2p-identify = { path = "../identify" } libp2p-noise = { workspace = true } -libp2p-swarm = { path = "../../swarm", features = ["macros", "async-std"] } +libp2p-swarm = { path = "../../swarm", features = ["macros", "tokio"] } libp2p-swarm-test = { path = "../../swarm-test" } libp2p-yamux = { workspace = true } quickcheck = { workspace = true } diff --git a/protocols/kad/src/behaviour.rs b/protocols/kad/src/behaviour.rs index ec7272a3d85..812f13e9110 100644 --- a/protocols/kad/src/behaviour.rs +++ b/protocols/kad/src/behaviour.rs @@ -382,13 +382,12 @@ impl Config { self } - /// Modifies the timeout duration of outbount_substreams. + /// Modifies the timeout duration of outbound substreams. /// /// * Default to `10` seconds. /// * May need to increase this value when sending large records with poor connection. - pub fn set_outbound_substreams_timeout(&mut self, timeout: Duration) -> &mut Self { - self.protocol_config - .set_outbound_substreams_timeout(timeout); + pub fn set_substreams_timeout(&mut self, timeout: Duration) -> &mut Self { + self.protocol_config.set_substreams_timeout(timeout); self } @@ -725,7 +724,7 @@ where /// Initiates an iterative query for the closest peers to the given key. /// /// The result of the query is delivered in a - /// [`Event::OutboundQueryProgressed{QueryResult::GetClosestPeers}`]. + /// [`Event::OutboundQueryProgressed`] with `result` [`QueryResult::GetClosestPeers`]. pub fn get_closest_peers(&mut self, key: K) -> QueryId where K: Into> + Into> + Clone, @@ -738,7 +737,7 @@ where /// Note that the result is capped after exceeds K_VALUE /// /// The result of the query is delivered in a - /// [`Event::OutboundQueryProgressed{QueryResult::GetClosestPeers}`]. + /// [`Event::OutboundQueryProgressed`] with `result` [`QueryResult::GetClosestPeers`]. pub fn get_n_closest_peers(&mut self, key: K, num_results: NonZeroUsize) -> QueryId where K: Into> + Into> + Clone, @@ -795,7 +794,7 @@ where /// Performs a lookup for a record in the DHT. /// /// The result of this operation is delivered in a - /// [`Event::OutboundQueryProgressed{QueryResult::GetRecord}`]. + /// [`Event::OutboundQueryProgressed`] with `result` [`QueryResult::GetRecord`]. pub fn get_record(&mut self, key: record::Key) -> QueryId { let record = if let Some(record) = self.store.get(&key) { if record.is_expired(Instant::now()) { @@ -824,7 +823,7 @@ where } else { QueryInfo::GetRecord { key, - step: step.clone(), + step, found_a_record: false, cache_candidates: BTreeMap::new(), } @@ -854,7 +853,7 @@ where /// Returns `Ok` if a record has been stored locally, providing the /// `QueryId` of the initial query that replicates the record in the DHT. /// The result of the query is eventually reported as a - /// [`Event::OutboundQueryProgressed{QueryResult::PutRecord}`]. + /// [`Event::OutboundQueryProgressed`] with `result` [`QueryResult::PutRecord`]. /// /// The record is always stored locally with the given expiration. If the record's /// expiration is `None`, the common case, it does not expire in local storage @@ -968,8 +967,8 @@ where /// /// Returns `Ok` if bootstrapping has been initiated with a self-lookup, providing the /// `QueryId` for the entire bootstrapping process. The progress of bootstrapping is - /// reported via [`Event::OutboundQueryProgressed{QueryResult::Bootstrap}`] events, - /// with one such event per bootstrapping query. + /// reported via [`Event::OutboundQueryProgressed`] with `result` [`QueryResult::Bootstrap`] + /// events, with one such event per bootstrapping query. /// /// Returns `Err` if bootstrapping is impossible due an empty routing table. /// @@ -1021,7 +1020,8 @@ where /// of the libp2p Kademlia provider API. /// /// The results of the (repeated) provider announcements sent by this node are - /// reported via [`Event::OutboundQueryProgressed{QueryResult::StartProviding}`]. + /// reported via [`Event::OutboundQueryProgressed`] with `result` + /// [`QueryResult::StartProviding`]. pub fn start_providing(&mut self, key: record::Key) -> Result { // Note: We store our own provider records locally without local addresses // to avoid redundant storage and outdated addresses. Instead these are @@ -1057,7 +1057,7 @@ where /// Performs a lookup for providers of a value to the given key. /// /// The result of this operation is delivered in a - /// reported via [`Event::OutboundQueryProgressed{QueryResult::GetProviders}`]. + /// reported via [`Event::OutboundQueryProgressed`] with `result` [`QueryResult::GetProviders`]. pub fn get_providers(&mut self, key: record::Key) -> QueryId { let providers: HashSet<_> = self .store @@ -1080,7 +1080,7 @@ where key: key.clone(), providers_found: providers.len(), step: if providers.is_empty() { - step.clone() + step } else { step.next() }, @@ -1231,7 +1231,7 @@ where let addrs = peer.multiaddrs.iter().cloned().collect(); query.peers.addresses.insert(peer.node_id, addrs); } - query.on_success(source, others_iter.cloned().map(|kp| kp.node_id)) + query.on_success(source, others_iter.map(|kp| kp.node_id)) } } @@ -2251,9 +2251,8 @@ where _addresses: &[Multiaddr], _effective_role: Endpoint, ) -> Result, ConnectionDenied> { - let peer_id = match maybe_peer { - None => return Ok(vec![]), - Some(peer) => peer, + let Some(peer_id) = maybe_peer else { + return Ok(vec![]); }; // We should order addresses from decreasing likelihood of connectivity, so start with @@ -2369,7 +2368,7 @@ where let peers = closer_peers.iter().chain(provider_peers.iter()); self.discovered(&query_id, &source, peers); if let Some(query) = self.queries.get_mut(&query_id) { - let stats = query.stats().clone(); + let stats = *query.stats(); if let QueryInfo::GetProviders { ref key, ref mut providers_found, @@ -2389,7 +2388,7 @@ where providers, }, )), - step: step.clone(), + step: *step, stats, }, )); @@ -2463,7 +2462,7 @@ where query_id, } => { if let Some(query) = self.queries.get_mut(&query_id) { - let stats = query.stats().clone(); + let stats = *query.stats(); if let QueryInfo::GetRecord { key, ref mut step, @@ -2484,7 +2483,7 @@ where result: QueryResult::GetRecord(Ok(GetRecordOk::FoundRecord( record, ))), - step: step.clone(), + step: *step, stats, }, )); @@ -2770,7 +2769,7 @@ pub enum Event { result: QueryResult, /// Execution statistics from the query. stats: QueryStats, - /// Indicates which event this is, if therer are multiple responses for a single query. + /// Indicates which event this is, if there are multiple responses for a single query. step: ProgressStep, }, @@ -2832,7 +2831,7 @@ pub enum Event { } /// Information about progress events. -#[derive(Debug, Clone)] +#[derive(Clone, Copy, Debug)] pub struct ProgressStep { /// The index into the event pub count: NonZeroUsize, @@ -2953,12 +2952,6 @@ pub enum GetRecordError { key: record::Key, closest_peers: Vec, }, - #[error("the quorum failed; needed {quorum} peers")] - QuorumFailed { - key: record::Key, - records: Vec, - quorum: NonZeroUsize, - }, #[error("the request timed out")] Timeout { key: record::Key }, } @@ -2967,7 +2960,6 @@ impl GetRecordError { /// Gets the key of the record for which the operation failed. pub fn key(&self) -> &record::Key { match self { - GetRecordError::QuorumFailed { key, .. } => key, GetRecordError::Timeout { key, .. } => key, GetRecordError::NotFound { key, .. } => key, } @@ -2977,7 +2969,6 @@ impl GetRecordError { /// consuming the error. pub fn into_key(self) -> record::Key { match self { - GetRecordError::QuorumFailed { key, .. } => key, GetRecordError::Timeout { key, .. } => key, GetRecordError::NotFound { key, .. } => key, } diff --git a/protocols/kad/src/behaviour/test.rs b/protocols/kad/src/behaviour/test.rs index 17df122f002..3819276d350 100644 --- a/protocols/kad/src/behaviour/test.rs +++ b/protocols/kad/src/behaviour/test.rs @@ -20,7 +20,7 @@ #![cfg(test)] -use futures::{executor::block_on, future::poll_fn, prelude::*}; +use futures::{future::poll_fn, prelude::*}; use futures_timer::Delay; use libp2p_core::{ multiaddr::{multiaddr, Protocol}, @@ -34,6 +34,7 @@ use libp2p_swarm::{self as swarm, Swarm, SwarmEvent}; use libp2p_yamux as yamux; use quickcheck::*; use rand::{random, rngs::StdRng, thread_rng, Rng, SeedableRng}; +use tokio::runtime::Runtime; use super::*; use crate::{ @@ -64,7 +65,7 @@ fn build_node_with_config(cfg: Config) -> (Multiaddr, TestSwarm) { transport, behaviour, local_id, - swarm::Config::with_async_std_executor(), + swarm::Config::with_tokio_executor(), ); let address: Multiaddr = Protocol::Memory(random::()).into(); @@ -187,7 +188,8 @@ fn bootstrap() { let mut first = true; // Run test - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for (i, swarm) in swarms.iter_mut().enumerate() { loop { match swarm.poll_next_unpin(ctx) { @@ -282,7 +284,8 @@ fn query_iter() { expected_distances.sort(); // Run test - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for (i, swarm) in swarms.iter_mut().enumerate() { loop { match swarm.poll_next_unpin(ctx) { @@ -345,7 +348,8 @@ fn unresponsive_not_returned_direct() { let search_target = PeerId::random(); swarms[0].behaviour_mut().get_closest_peers(search_target); - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for swarm in &mut swarms { loop { match swarm.poll_next_unpin(ctx) { @@ -403,7 +407,8 @@ fn unresponsive_not_returned_indirect() { let search_target = PeerId::random(); swarms[1].behaviour_mut().get_closest_peers(search_target); - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for swarm in &mut swarms { loop { match swarm.poll_next_unpin(ctx) { @@ -461,7 +466,8 @@ fn get_closest_with_different_num_results_inner(num_results: usize, replication_ .behaviour_mut() .get_n_closest_peers(search_target, num_results_nonzero); - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for swarm in &mut swarms { loop { match swarm.poll_next_unpin(ctx) { @@ -518,7 +524,8 @@ fn get_record_not_found() { let target_key = record::Key::from(random_multihash()); let qid = swarms[0].behaviour_mut().get_record(target_key.clone()); - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for swarm in &mut swarms { loop { match swarm.poll_next_unpin(ctx) { @@ -639,7 +646,8 @@ fn put_record() { // The accumulated results for one round of publishing. let mut results = Vec::new(); - block_on(poll_fn(move |ctx| loop { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| loop { // Poll all swarms until they are "Pending". for swarm in &mut swarms { loop { @@ -830,7 +838,8 @@ fn get_record() { swarms[2].behaviour_mut().store.put(record.clone()).unwrap(); let qid = swarms[0].behaviour_mut().get_record(record.key.clone()); - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for swarm in &mut swarms { loop { match swarm.poll_next_unpin(ctx) { @@ -887,7 +896,8 @@ fn get_record_many() { let quorum = Quorum::N(NonZeroUsize::new(num_results).unwrap()); let qid = swarms[0].behaviour_mut().get_record(record.key.clone()); - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for (i, swarm) in swarms.iter_mut().enumerate() { let mut records = Vec::new(); let quorum = quorum.eval(swarm.behaviour().queries.config().replication_factor); @@ -987,7 +997,8 @@ fn add_provider() { qids.insert(qid); } - block_on(poll_fn(move |ctx| loop { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| loop { // Poll all swarms until they are "Pending". for swarm in &mut swarms { loop { @@ -1125,7 +1136,8 @@ fn exceed_jobs_max_queries() { assert_eq!(swarm.behaviour_mut().queries.size(), num); - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for _ in 0..num { // There are no other nodes, so the queries finish instantly. loop { @@ -1210,7 +1222,8 @@ fn disjoint_query_does_not_finish_before_all_paths_did() { // Poll only `alice` and `trudy` expecting `alice` not yet to return a query // result as it is not able to connect to `bob` just yet. let addr_trudy = *Swarm::local_peer_id(&trudy); - block_on(poll_fn(|ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(|ctx| { for (i, swarm) in [&mut alice, &mut trudy].iter_mut().enumerate() { loop { match swarm.poll_next_unpin(ctx) { @@ -1264,7 +1277,7 @@ fn disjoint_query_does_not_finish_before_all_paths_did() { // Poll `alice` and `bob` expecting `alice` to return a successful query // result as it is now able to explore the second disjoint path. - let records = block_on(poll_fn(|ctx| { + let records = rt.block_on(poll_fn(|ctx| { let mut records = Vec::new(); for (i, swarm) in [&mut alice, &mut bob].iter_mut().enumerate() { loop { @@ -1337,7 +1350,8 @@ fn manual_bucket_inserts() { .1 .behaviour_mut() .get_closest_peers(PeerId::random()); - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for (_, swarm) in swarms.iter_mut() { loop { match swarm.poll_next_unpin(ctx) { @@ -1460,7 +1474,8 @@ fn get_providers_single() { .start_providing(key.clone()) .expect("could not provide"); - block_on(async { + let rt = Runtime::new().unwrap(); + rt.block_on(async { match single_swarm.next().await.unwrap() { SwarmEvent::Behaviour(Event::OutboundQueryProgressed { result: QueryResult::StartProviding(Ok(_)), @@ -1474,7 +1489,7 @@ fn get_providers_single() { let query_id = single_swarm.behaviour_mut().get_providers(key); - block_on(async { + rt.block_on(async { loop { match single_swarm.next().await.unwrap() { SwarmEvent::Behaviour(Event::OutboundQueryProgressed { @@ -1538,7 +1553,8 @@ fn get_providers_limit() { let mut all_providers: Vec = vec![]; - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for (i, swarm) in swarms.iter_mut().enumerate() { loop { match swarm.poll_next_unpin(ctx) { @@ -1630,7 +1646,8 @@ fn get_closest_peers_should_return_up_to_k_peers() { let search_target = PeerId::random(); swarms[0].behaviour_mut().get_closest_peers(search_target); - block_on(poll_fn(move |ctx| { + let rt = Runtime::new().unwrap(); + rt.block_on(poll_fn(move |ctx| { for swarm in &mut swarms { loop { match swarm.poll_next_unpin(ctx) { diff --git a/protocols/kad/src/handler.rs b/protocols/kad/src/handler.rs index 5edbd9b2df4..2c7b6c52257 100644 --- a/protocols/kad/src/handler.rs +++ b/protocols/kad/src/handler.rs @@ -453,7 +453,7 @@ impl Handler { } } - let outbound_substreams_timeout = protocol_config.outbound_substreams_timeout_s(); + let substreams_timeout = protocol_config.substreams_timeout_s(); Handler { protocol_config, @@ -463,7 +463,7 @@ impl Handler { next_connec_unique_id: UniqueConnecId(0), inbound_substreams: Default::default(), outbound_substreams: futures_bounded::FuturesTupleSet::new( - outbound_substreams_timeout, + substreams_timeout, MAX_NUM_STREAMS, ), pending_streams: Default::default(), @@ -820,14 +820,11 @@ fn compute_new_protocol_status( now_supported: bool, current_status: Option, ) -> ProtocolStatus { - let current_status = match current_status { - None => { - return ProtocolStatus { - supported: now_supported, - reported: false, - } - } - Some(current) => current, + let Some(current_status) = current_status else { + return ProtocolStatus { + supported: now_supported, + reported: false, + }; }; if now_supported == current_status.supported { diff --git a/protocols/kad/src/protocol.rs b/protocols/kad/src/protocol.rs index 4be53dcd0e4..df181af1a86 100644 --- a/protocols/kad/src/protocol.rs +++ b/protocols/kad/src/protocol.rs @@ -50,7 +50,7 @@ pub(crate) const DEFAULT_PROTO_NAME: StreamProtocol = StreamProtocol::new("/ipfs /// The default maximum size for a varint length-delimited packet. pub(crate) const DEFAULT_MAX_PACKET_SIZE: usize = 16 * 1024; /// The default timeout of outbound_substreams to be 10 (seconds). -const DEFAULT_OUTBOUND_SUBSTREAMS_TIMEOUT_S: Duration = Duration::from_secs(10); +const DEFAULT_SUBSTREAMS_TIMEOUT_S: Duration = Duration::from_secs(10); /// Status of our connection to a node reported by the Kademlia protocol. #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub enum ConnectionType { @@ -148,7 +148,7 @@ pub struct ProtocolConfig { /// Maximum allowed size of a packet. max_packet_size: usize, /// Specifies the outbound_substreams timeout in seconds - outbound_substreams_timeout_s: Duration, + substreams_timeout_s: Duration, } impl ProtocolConfig { @@ -157,7 +157,7 @@ impl ProtocolConfig { ProtocolConfig { protocol_names: vec![protocol_name], max_packet_size: DEFAULT_MAX_PACKET_SIZE, - outbound_substreams_timeout_s: DEFAULT_OUTBOUND_SUBSTREAMS_TIMEOUT_S, + substreams_timeout_s: DEFAULT_SUBSTREAMS_TIMEOUT_S, } } @@ -171,14 +171,14 @@ impl ProtocolConfig { self.max_packet_size = size; } - /// Modifies outbount_substreams timeout. - pub fn set_outbound_substreams_timeout(&mut self, timeout: Duration) { - self.outbound_substreams_timeout_s = timeout; + /// Modifies the outbound substreams timeout. + pub fn set_substreams_timeout(&mut self, timeout: Duration) { + self.substreams_timeout_s = timeout; } - /// Getter of outbount_substreams_timeout_s. - pub fn outbound_substreams_timeout_s(&self) -> Duration { - self.outbound_substreams_timeout_s + /// Getter of substreams_timeout_s. + pub fn substreams_timeout_s(&self) -> Duration { + self.substreams_timeout_s } } diff --git a/protocols/kad/src/query.rs b/protocols/kad/src/query.rs index 053aec4a369..5b2035c993e 100644 --- a/protocols/kad/src/query.rs +++ b/protocols/kad/src/query.rs @@ -464,7 +464,7 @@ impl Query { } /// Execution statistics of a query. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct QueryStats { requests: u32, success: u32, diff --git a/protocols/kad/src/query/peers/closest.rs b/protocols/kad/src/query/peers/closest.rs index 2d1f91f050c..c4d90d89e6c 100644 --- a/protocols/kad/src/query/peers/closest.rs +++ b/protocols/kad/src/query/peers/closest.rs @@ -713,7 +713,7 @@ mod tests { _ => panic!("No peer."), }; iter.on_success(&peer1, closer.clone()); - // Duplicate result from te same peer. + // Duplicate result from the same peer. iter.on_success(&peer1, closer.clone()); // If there is a second peer, let it also report the same "closer" peer. diff --git a/protocols/mdns/CHANGELOG.md b/protocols/mdns/CHANGELOG.md index 7f984208046..a1d928c5658 100644 --- a/protocols/mdns/CHANGELOG.md +++ b/protocols/mdns/CHANGELOG.md @@ -2,6 +2,8 @@ - Remove `async_std` dependency [PR 5958](https://github.com/libp2p/rust-libp2p/pull/5958) + + ## 0.47.0 - Emit `ToSwarm::NewExternalAddrOfPeer` on discovery. diff --git a/protocols/mdns/Cargo.toml b/protocols/mdns/Cargo.toml index ad489a8991a..5f3f9dfeccf 100644 --- a/protocols/mdns/Cargo.toml +++ b/protocols/mdns/Cargo.toml @@ -18,7 +18,7 @@ libp2p-swarm = { workspace = true } libp2p-identity = { workspace = true } rand = "0.8.3" smallvec = "1.13.2" -socket2 = { version = "0.5.7", features = ["all"] } +socket2 = { version = "0.6.0", features = ["all"] } tokio = { workspace = true, default-features = false, features = ["net", "time"], optional = true } tracing = { workspace = true } hickory-proto = { workspace = true, features = ["mdns"] } diff --git a/protocols/mdns/src/behaviour.rs b/protocols/mdns/src/behaviour.rs index 82af16fe1be..a6383e159f4 100644 --- a/protocols/mdns/src/behaviour.rs +++ b/protocols/mdns/src/behaviour.rs @@ -229,9 +229,8 @@ where _addresses: &[Multiaddr], _effective_role: Endpoint, ) -> Result, ConnectionDenied> { - let peer_id = match maybe_peer { - None => return Ok(vec![]), - Some(peer) => peer, + let Some(peer_id) = maybe_peer else { + return Ok(vec![]); }; Ok(self diff --git a/protocols/perf/CHANGELOG.md b/protocols/perf/CHANGELOG.md index 52318bbeb28..2507cf3ee4d 100644 --- a/protocols/perf/CHANGELOG.md +++ b/protocols/perf/CHANGELOG.md @@ -1,6 +1,5 @@ ## 0.4.0 - - Add ConnectionError to FromSwarm::ConnectionClosed. See [PR 5485](https://github.com/libp2p/rust-libp2p/pull/5485). @@ -8,6 +7,7 @@ See [PR 5676](https://github.com/libp2p/rust-libp2p/pull/5676). + ## 0.3.1 - Use `web-time` instead of `instant`. diff --git a/protocols/ping/CHANGELOG.md b/protocols/ping/CHANGELOG.md index 1131d112b3d..346a2c93196 100644 --- a/protocols/ping/CHANGELOG.md +++ b/protocols/ping/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.47.0 + + + ## 0.46.0 - Deprecate `void` crate. diff --git a/protocols/ping/Cargo.toml b/protocols/ping/Cargo.toml index 223a8eee602..ddce660ddd6 100644 --- a/protocols/ping/Cargo.toml +++ b/protocols/ping/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-ping" edition.workspace = true rust-version = { workspace = true } description = "Ping protocol for libp2p" -version = "0.46.0" +version = "0.47.0" authors = ["Parity Technologies "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" diff --git a/protocols/relay/CHANGELOG.md b/protocols/relay/CHANGELOG.md index d14e567137b..fde8a2a6807 100644 --- a/protocols/relay/CHANGELOG.md +++ b/protocols/relay/CHANGELOG.md @@ -1,8 +1,19 @@ +## 0.21.1 + +- reduce allocations by replacing `get_or_insert` with `get_or_insert_with` + See [PR 6136](https://github.com/libp2p/rust-libp2p/pull/6136) + +## 0.21.0 + + + ## 0.20.0 - Remove duplicated forwarding of pending events to connection handler. - Emit `relay::Event::ReservationClosed` when an active reservation is dropped due to the connection closing. See [PR 5869](https://github.com/libp2p/rust-libp2p/pull/5869). +- Include denial reason in `relay::Event::CircuitReqDenied` and `relay::Event::ReservationReqDenied` + See [PR 6067](https://github.com/libp2p/rust-libp2p/pull/6067/files). ## 0.19.0 diff --git a/protocols/relay/Cargo.toml b/protocols/relay/Cargo.toml index ca5c57bce5a..3871abbcf8a 100644 --- a/protocols/relay/Cargo.toml +++ b/protocols/relay/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-relay" edition.workspace = true rust-version = { workspace = true } description = "Communications relaying for libp2p" -version = "0.20.0" +version = "0.21.1" authors = ["Parity Technologies ", "Max Inden "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" @@ -32,11 +32,12 @@ tracing = { workspace = true } libp2p-identity = { workspace = true, features = ["rand"] } libp2p-ping = { workspace = true } libp2p-plaintext = { workspace = true } -libp2p-swarm = { workspace = true, features = ["macros", "async-std"] } +libp2p-swarm = { workspace = true, features = ["macros", "tokio"] } libp2p-swarm-test = { workspace = true } libp2p-yamux = { workspace = true } quickcheck = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } # Passing arguments to the docsrs builder in order to properly document cfg's. diff --git a/protocols/relay/src/behaviour.rs b/protocols/relay/src/behaviour.rs index 9c768c6022d..fe6b08c876a 100644 --- a/protocols/relay/src/behaviour.rs +++ b/protocols/relay/src/behaviour.rs @@ -184,7 +184,10 @@ pub enum Event { error: inbound_hop::Error, }, /// An inbound reservation request has been denied. - ReservationReqDenied { src_peer_id: PeerId }, + ReservationReqDenied { + src_peer_id: PeerId, + status: StatusCode, + }, /// Denying an inbound reservation request has failed. #[deprecated( note = "Will be removed in favor of logging them internally, see for details." @@ -201,6 +204,7 @@ pub enum Event { CircuitReqDenied { src_peer_id: PeerId, dst_peer_id: PeerId, + status: StatusCode, }, /// Denying an inbound circuit request failed. #[deprecated( @@ -481,10 +485,11 @@ impl NetworkBehaviour for Behaviour { }, )); } - handler::Event::ReservationReqDenied {} => { + handler::Event::ReservationReqDenied { status } => { self.queued_actions.push_back(ToSwarm::GenerateEvent( Event::ReservationReqDenied { src_peer_id: event_source, + status: status.into(), }, )); } @@ -592,6 +597,7 @@ impl NetworkBehaviour for Behaviour { handler::Event::CircuitReqDenied { circuit_id, dst_peer_id, + status, } => { if let Some(circuit_id) = circuit_id { self.circuits.remove(circuit_id); @@ -601,6 +607,7 @@ impl NetworkBehaviour for Behaviour { .push_back(ToSwarm::GenerateEvent(Event::CircuitReqDenied { src_peer_id: event_source, dst_peer_id, + status: status.into(), })); } handler::Event::CircuitReqDenyFailed { @@ -809,3 +816,31 @@ impl Add for CircuitId { CircuitId(self.0 + rhs) } } + +/// Status code for a relay reservation request that was denied. +#[derive(Debug)] +pub enum StatusCode { + OK, + ReservationRefused, + ResourceLimitExceeded, + PermissionDenied, + ConnectionFailed, + NoReservation, + MalformedMessage, + UnexpectedMessage, +} + +impl From for StatusCode { + fn from(other: proto::Status) -> Self { + match other { + proto::Status::OK => Self::OK, + proto::Status::RESERVATION_REFUSED => Self::ReservationRefused, + proto::Status::RESOURCE_LIMIT_EXCEEDED => Self::ResourceLimitExceeded, + proto::Status::PERMISSION_DENIED => Self::PermissionDenied, + proto::Status::CONNECTION_FAILED => Self::ConnectionFailed, + proto::Status::NO_RESERVATION => Self::NoReservation, + proto::Status::MALFORMED_MESSAGE => Self::MalformedMessage, + proto::Status::UNEXPECTED_MESSAGE => Self::UnexpectedMessage, + } + } +} diff --git a/protocols/relay/src/behaviour/handler.rs b/protocols/relay/src/behaviour/handler.rs index fb80112e71c..af130c35516 100644 --- a/protocols/relay/src/behaviour/handler.rs +++ b/protocols/relay/src/behaviour/handler.rs @@ -159,7 +159,7 @@ pub enum Event { /// Accepting an inbound reservation request failed. ReservationReqAcceptFailed { error: inbound_hop::Error }, /// An inbound reservation request has been denied. - ReservationReqDenied {}, + ReservationReqDenied { status: proto::Status }, /// Denying an inbound reservation request has failed. ReservationReqDenyFailed { error: inbound_hop::Error }, /// An inbound reservation has timed out. @@ -173,6 +173,7 @@ pub enum Event { CircuitReqDenied { circuit_id: Option, dst_peer_id: PeerId, + status: proto::Status, }, /// Denying an inbound circuit request failed. CircuitReqDenyFailed { @@ -238,9 +239,10 @@ impl fmt::Debug for Event { .debug_struct("Event::ReservationReqAcceptFailed") .field("error", error) .finish(), - Event::ReservationReqDenied {} => { - f.debug_struct("Event::ReservationReqDenied").finish() - } + Event::ReservationReqDenied { status } => f + .debug_struct("Event::ReservationReqDenied") + .field("status", status) + .finish(), Event::ReservationReqDenyFailed { error } => f .debug_struct("Event::ReservationReqDenyFailed") .field("error", error) @@ -256,10 +258,12 @@ impl fmt::Debug for Event { Event::CircuitReqDenied { circuit_id, dst_peer_id, + status, } => f .debug_struct("Event::CircuitReqDenied") .field("circuit_id", circuit_id) .field("dst_peer_id", dst_peer_id) + .field("status", status) .finish(), Event::CircuitReqDenyFailed { circuit_id, @@ -359,7 +363,12 @@ pub struct Handler { /// Futures accepting an inbound circuit request. circuit_accept_futures: Futures>, /// Futures denying an inbound circuit request. - circuit_deny_futures: Futures<(Option, PeerId, Result<(), inbound_hop::Error>)>, + circuit_deny_futures: Futures<( + Option, + PeerId, + proto::Status, + Result<(), inbound_hop::Error>, + )>, /// Futures relaying data for circuit between two peers. circuits: Futures<(CircuitId, PeerId, Result<(), std::io::Error>)>, @@ -479,7 +488,7 @@ impl Handler { enum ReservationRequestFuture { Accepting(BoxFuture<'static, Result<(), inbound_hop::Error>>), - Denying(BoxFuture<'static, Result<(), inbound_hop::Error>>), + Denying(BoxFuture<'static, (proto::Status, Result<(), inbound_hop::Error>)>), } type Futures = FuturesUnordered>; @@ -519,7 +528,11 @@ impl ConnectionHandler for Handler { if self .reservation_request_future .replace(ReservationRequestFuture::Denying( - inbound_reservation_req.deny(status).err_into().boxed(), + inbound_reservation_req + .deny(status) + .err_into() + .map(move |result| (status, result)) + .boxed(), )) .is_some() { @@ -554,7 +567,7 @@ impl ConnectionHandler for Handler { inbound_circuit_req .deny(status) .err_into() - .map(move |result| (circuit_id, dst_peer_id, result)) + .map(move |result| (circuit_id, dst_peer_id, status, result)) .boxed(), ); } @@ -719,7 +732,7 @@ impl ConnectionHandler for Handler { } // Deny new circuits. - if let Poll::Ready(Some((circuit_id, dst_peer_id, result))) = + if let Poll::Ready(Some((circuit_id, dst_peer_id, status, result))) = self.circuit_deny_futures.poll_next_unpin(cx) { match result { @@ -728,6 +741,7 @@ impl ConnectionHandler for Handler { Event::CircuitReqDenied { circuit_id, dst_peer_id, + status, }, )); } @@ -838,13 +852,13 @@ impl ConnectionHandler for Handler { } } Some(ReservationRequestFuture::Denying(fut)) => { - if let Poll::Ready(result) = fut.poll_unpin(cx) { + if let Poll::Ready((status, result)) = fut.poll_unpin(cx) { self.reservation_request_future = None; match result { Ok(()) => { return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour( - Event::ReservationReqDenied {}, + Event::ReservationReqDenied { status }, )) } Err(error) => { diff --git a/protocols/relay/src/lib.rs b/protocols/relay/src/lib.rs index dba07015765..515fb40ef4b 100644 --- a/protocols/relay/src/lib.rs +++ b/protocols/relay/src/lib.rs @@ -39,7 +39,7 @@ mod proto { }; } -pub use behaviour::{rate_limiter::RateLimiter, Behaviour, CircuitId, Config, Event}; +pub use behaviour::{rate_limiter::RateLimiter, Behaviour, CircuitId, Config, Event, StatusCode}; pub use protocol::{HOP_PROTOCOL_NAME, STOP_PROTOCOL_NAME}; /// Types related to the relay protocol inbound. diff --git a/protocols/relay/src/priv_client/transport.rs b/protocols/relay/src/priv_client/transport.rs index ed9faa946db..c5c17fd5137 100644 --- a/protocols/relay/src/priv_client/transport.rs +++ b/protocols/relay/src/priv_client/transport.rs @@ -297,12 +297,12 @@ fn parse_relayed_multiaddr(addr: Multiaddr) -> Result())); let mut relay = build_relay(); @@ -55,7 +51,9 @@ fn reservation() { relay.listen_on(relay_addr.clone()).unwrap(); relay.add_external_address(relay_addr.clone()); - spawn_swarm_on_pool(&pool, relay); + tokio::spawn(async move { + relay.collect::>().await; + }); let client_addr = relay_addr .with(Protocol::P2p(relay_peer_id)) @@ -66,31 +64,32 @@ fn reservation() { client.listen_on(client_addr.clone()).unwrap(); // Wait for connection to relay. - assert!(pool.run_until(wait_for_dial(&mut client, relay_peer_id))); + assert!(wait_for_dial(&mut client, relay_peer_id).await); // Wait for initial reservation. - pool.run_until(wait_for_reservation( + wait_for_reservation( &mut client, client_addr.clone().with(Protocol::P2p(client_peer_id)), relay_peer_id, false, // No renewal. - )); + ) + .await; // Wait for renewal. - pool.run_until(wait_for_reservation( + wait_for_reservation( &mut client, client_addr.with(Protocol::P2p(client_peer_id)), relay_peer_id, true, // Renewal. - )); + ) + .await; } -#[test] -fn new_reservation_to_same_relay_replaces_old() { +#[tokio::test] +async fn new_reservation_to_same_relay_replaces_old() { let _ = tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .try_init(); - let mut pool = LocalPool::new(); let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); let mut relay = build_relay(); @@ -98,7 +97,9 @@ fn new_reservation_to_same_relay_replaces_old() { relay.listen_on(relay_addr.clone()).unwrap(); relay.add_external_address(relay_addr.clone()); - spawn_swarm_on_pool(&pool, relay); + tokio::spawn(async move { + relay.collect::>().await; + }); let mut client = build_client(); let client_peer_id = *client.local_peer_id(); @@ -110,15 +111,16 @@ fn new_reservation_to_same_relay_replaces_old() { let old_listener = client.listen_on(client_addr.clone()).unwrap(); // Wait for connection to relay. - assert!(pool.run_until(wait_for_dial(&mut client, relay_peer_id))); + assert!(wait_for_dial(&mut client, relay_peer_id).await); // Wait for first (old) reservation. - pool.run_until(wait_for_reservation( + wait_for_reservation( &mut client, client_addr_with_peer_id.clone(), relay_peer_id, false, // No renewal. - )); + ) + .await; // Trigger new reservation. let new_listener = client.listen_on(client_addr.clone()).unwrap(); @@ -127,69 +129,66 @@ fn new_reservation_to_same_relay_replaces_old() { // - listener of old reservation to close // - new reservation to be accepted // - new listener address to be reported - pool.run_until(async { - let mut old_listener_closed = false; - let mut new_reservation_accepted = false; - let mut new_listener_address_reported = false; - loop { - match client.select_next_some().await { - SwarmEvent::ListenerClosed { - addresses, - listener_id, + let mut old_listener_closed = false; + let mut new_reservation_accepted = false; + let mut new_listener_address_reported = false; + loop { + match client.select_next_some().await { + SwarmEvent::ListenerClosed { + addresses, + listener_id, + .. + } => { + assert_eq!(addresses, vec![client_addr_with_peer_id.clone()]); + assert_eq!(listener_id, old_listener); + + old_listener_closed = true; + if new_reservation_accepted && new_listener_address_reported { + break; + } + } + SwarmEvent::Behaviour(ClientEvent::Relay( + relay::client::Event::ReservationReqAccepted { + relay_peer_id: peer_id, .. - } => { - assert_eq!(addresses, vec![client_addr_with_peer_id.clone()]); - assert_eq!(listener_id, old_listener); - - old_listener_closed = true; - if new_reservation_accepted && new_listener_address_reported { - break; - } + }, + )) => { + assert_eq!(relay_peer_id, peer_id); + + new_reservation_accepted = true; + if old_listener_closed && new_listener_address_reported { + break; } - SwarmEvent::Behaviour(ClientEvent::Relay( - relay::client::Event::ReservationReqAccepted { - relay_peer_id: peer_id, - .. - }, - )) => { - assert_eq!(relay_peer_id, peer_id); - - new_reservation_accepted = true; - if old_listener_closed && new_listener_address_reported { - break; - } + } + SwarmEvent::NewListenAddr { + address, + listener_id, + } => { + assert_eq!(address, client_addr_with_peer_id); + assert_eq!(listener_id, new_listener); + + new_listener_address_reported = true; + if old_listener_closed && new_reservation_accepted { + break; } - SwarmEvent::NewListenAddr { + } + SwarmEvent::ExternalAddrConfirmed { address } => { + assert_eq!( address, - listener_id, - } => { - assert_eq!(address, client_addr_with_peer_id); - assert_eq!(listener_id, new_listener); - - new_listener_address_reported = true; - if old_listener_closed && new_reservation_accepted { - break; - } - } - SwarmEvent::ExternalAddrConfirmed { address } => { - assert_eq!( - address, - client_addr.clone().with(Protocol::P2p(client_peer_id)) - ); - } - SwarmEvent::Behaviour(ClientEvent::Ping(_)) => {} - e => panic!("{e:?}"), + client_addr.clone().with(Protocol::P2p(client_peer_id)) + ); } + SwarmEvent::Behaviour(ClientEvent::Ping(_)) => {} + e => panic!("{e:?}"), } - }); + } } -#[test] -fn connect() { +#[tokio::test] +async fn connect() { let _ = tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .try_init(); - let mut pool = LocalPool::new(); let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); let mut relay = build_relay(); @@ -197,7 +196,9 @@ fn connect() { relay.listen_on(relay_addr.clone()).unwrap(); relay.add_external_address(relay_addr.clone()); - spawn_swarm_on_pool(&pool, relay); + tokio::spawn(async move { + relay.collect::>().await; + }); let mut dst = build_client(); let dst_peer_id = *dst.local_peer_id(); @@ -208,24 +209,26 @@ fn connect() { dst.listen_on(dst_addr.clone()).unwrap(); - assert!(pool.run_until(wait_for_dial(&mut dst, relay_peer_id))); + assert!(wait_for_dial(&mut dst, relay_peer_id).await); - pool.run_until(wait_for_reservation( + wait_for_reservation( &mut dst, dst_addr.clone(), relay_peer_id, false, // No renewal. - )); + ) + .await; let mut src = build_client(); let src_peer_id = *src.local_peer_id(); src.dial(dst_addr).unwrap(); - pool.run_until(futures::future::join( + futures::future::join( connection_established_to(&mut src, relay_peer_id, dst_peer_id), connection_established_to(&mut dst, relay_peer_id, src_peer_id), - )); + ) + .await; } async fn connection_established_to( @@ -270,12 +273,11 @@ async fn connection_established_to( } } -#[test] -fn handle_dial_failure() { +#[tokio::test] +async fn handle_dial_failure() { let _ = tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .try_init(); - let mut pool = LocalPool::new(); let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); let relay_peer_id = PeerId::random(); @@ -288,15 +290,14 @@ fn handle_dial_failure() { .with(Protocol::P2p(client_peer_id)); client.listen_on(client_addr).unwrap(); - assert!(!pool.run_until(wait_for_dial(&mut client, relay_peer_id))); + assert!(!wait_for_dial(&mut client, relay_peer_id).await); } -#[test] -fn propagate_reservation_error_to_listener() { +#[tokio::test] +async fn propagate_reservation_error_to_listener() { let _ = tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .try_init(); - let mut pool = LocalPool::new(); let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); let mut relay = build_relay_with_config(relay::Config { @@ -307,7 +308,9 @@ fn propagate_reservation_error_to_listener() { relay.listen_on(relay_addr.clone()).unwrap(); relay.add_external_address(relay_addr.clone()); - spawn_swarm_on_pool(&pool, relay); + tokio::spawn(async move { + relay.collect::>().await; + }); let client_addr = relay_addr .with(Protocol::P2p(relay_peer_id)) @@ -317,16 +320,18 @@ fn propagate_reservation_error_to_listener() { let reservation_listener = client.listen_on(client_addr.clone()).unwrap(); // Wait for connection to relay. - assert!(pool.run_until(wait_for_dial(&mut client, relay_peer_id))); + assert!(wait_for_dial(&mut client, relay_peer_id).await); - let error = pool.run_until(client.wait(|e| match e { - SwarmEvent::ListenerClosed { - listener_id, - reason: Err(e), - .. - } if listener_id == reservation_listener => Some(e), - _ => None, - })); + let error = client + .wait(|e| match e { + SwarmEvent::ListenerClosed { + listener_id, + reason: Err(e), + .. + } if listener_id == reservation_listener => Some(e), + _ => None, + }) + .await; let error = error .source() @@ -340,12 +345,11 @@ fn propagate_reservation_error_to_listener() { )); } -#[test] -fn propagate_connect_error_to_unknown_peer_to_dialer() { +#[tokio::test] +async fn propagate_connect_error_to_unknown_peer_to_dialer() { let _ = tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .try_init(); - let mut pool = LocalPool::new(); let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); let mut relay = build_relay(); @@ -353,7 +357,9 @@ fn propagate_connect_error_to_unknown_peer_to_dialer() { relay.listen_on(relay_addr.clone()).unwrap(); relay.add_external_address(relay_addr.clone()); - spawn_swarm_on_pool(&pool, relay); + tokio::spawn(async move { + relay.collect::>().await; + }); let mut src = build_client(); @@ -368,17 +374,19 @@ fn propagate_connect_error_to_unknown_peer_to_dialer() { src.dial(opts).unwrap(); - let (failed_address, error) = pool.run_until(src.wait(|e| match e { - SwarmEvent::OutgoingConnectionError { - connection_id, - error: DialError::Transport(mut errors), - .. - } if connection_id == circuit_connection_id => { - assert_eq!(errors.len(), 1); - Some(errors.remove(0)) - } - _ => None, - })); + let (failed_address, error) = src + .wait(|e| match e { + SwarmEvent::OutgoingConnectionError { + connection_id, + error: DialError::Transport(mut errors), + .. + } if connection_id == circuit_connection_id => { + assert_eq!(errors.len(), 1); + Some(errors.remove(0)) + } + _ => None, + }) + .await; // This is a bit wonky but we need to get the _actual_ source error :) let error = error @@ -396,12 +404,11 @@ fn propagate_connect_error_to_unknown_peer_to_dialer() { )); } -#[test] -fn reuse_connection() { +#[tokio::test] +async fn reuse_connection() { let _ = tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .try_init(); - let mut pool = LocalPool::new(); let relay_addr = Multiaddr::empty().with(Protocol::Memory(rand::random::())); let mut relay = build_relay(); @@ -409,7 +416,9 @@ fn reuse_connection() { relay.listen_on(relay_addr.clone()).unwrap(); relay.add_external_address(relay_addr.clone()); - spawn_swarm_on_pool(&pool, relay); + tokio::spawn(async move { + relay.collect::>().await; + }); let client_addr = relay_addr .clone() @@ -420,16 +429,17 @@ fn reuse_connection() { let client_peer_id = *client.local_peer_id(); client.dial(relay_addr).unwrap(); - assert!(pool.run_until(wait_for_dial(&mut client, relay_peer_id))); + assert!(wait_for_dial(&mut client, relay_peer_id).await); client.listen_on(client_addr.clone()).unwrap(); - pool.run_until(wait_for_reservation( + wait_for_reservation( &mut client, client_addr.with(Protocol::P2p(client_peer_id)), relay_peer_id, false, // No renewal. - )); + ) + .await; } fn build_relay() -> Swarm { @@ -452,12 +462,12 @@ fn build_relay_with_config(config: relay::Config) -> Swarm { relay: relay::Behaviour::new(local_peer_id, config), }, local_peer_id, - Config::with_async_std_executor(), + Config::with_tokio_executor(), ) } fn build_client() -> Swarm { - build_client_with_config(Config::with_async_std_executor()) + build_client_with_config(Config::with_tokio_executor()) } fn build_client_with_config(config: Config) -> Swarm { @@ -509,12 +519,6 @@ struct Client { ping: ping::Behaviour, } -fn spawn_swarm_on_pool(pool: &LocalPool, swarm: Swarm) { - pool.spawner() - .spawn_obj(swarm.collect::>().map(|_| ()).boxed().into()) - .unwrap(); -} - async fn wait_for_reservation( client: &mut Swarm, client_addr: Multiaddr, diff --git a/protocols/rendezvous/CHANGELOG.md b/protocols/rendezvous/CHANGELOG.md index b8752be7159..77727945347 100644 --- a/protocols/rendezvous/CHANGELOG.md +++ b/protocols/rendezvous/CHANGELOG.md @@ -1,9 +1,12 @@ -## 0.16.1 +## 0.17.0 + - Emit `ToSwarm::NewExternalAddrOfPeer` for newly discovered peers. See [PR 5138](https://github.com/libp2p/rust-libp2p/pull/5138). - Log error instead of panicking when sending response to channel fails See [PR 6002](https://github.com/libp2p/rust-libp2p/pull/6002). + + ## 0.16.0 - Update to `libp2p-request-response` `v0.28.0`. diff --git a/protocols/rendezvous/Cargo.toml b/protocols/rendezvous/Cargo.toml index 316bbebb029..d80db65a27c 100644 --- a/protocols/rendezvous/Cargo.toml +++ b/protocols/rendezvous/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-rendezvous" edition.workspace = true rust-version = { workspace = true } description = "Rendezvous protocol for libp2p" -version = "0.16.1" +version = "0.17.0" authors = ["The COMIT guys "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" diff --git a/protocols/request-response/CHANGELOG.md b/protocols/request-response/CHANGELOG.md index dc08997330b..464c0e2b169 100644 --- a/protocols/request-response/CHANGELOG.md +++ b/protocols/request-response/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.28.1 +## 0.29.0 - fix: public cbor/json codec module See [PR 5830](https://github.com/libp2p/rust-libp2p/pull/5830). @@ -9,6 +9,8 @@ - fix: don't fail outbound request on `DialError::DialPeerConditionFalse`. See [PR 6000](https://github.com/libp2p/rust-libp2p/pull/6000) + + ## 0.28.0 - Deprecate `void` crate. diff --git a/protocols/request-response/Cargo.toml b/protocols/request-response/Cargo.toml index 4f82da730ee..253784a0ccd 100644 --- a/protocols/request-response/Cargo.toml +++ b/protocols/request-response/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-request-response" edition.workspace = true rust-version = { workspace = true } description = "Generic Request/Response Protocols" -version = "0.28.1" +version = "0.29.0" authors = ["Parity Technologies "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" @@ -32,7 +32,7 @@ cbor = ["dep:serde", "dep:cbor4ii", "libp2p-swarm/macros"] anyhow = "1.0.86" tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } rand = "0.8" -libp2p-swarm-test = { path = "../../swarm-test", features = ["async-std"]} +libp2p-swarm-test = { path = "../../swarm-test" } futures_ringbuf = "0.4.0" serde = { version = "1.0", features = ["derive"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/protocols/request-response/src/lib.rs b/protocols/request-response/src/lib.rs index e46f43e41a0..1ac781b5744 100644 --- a/protocols/request-response/src/lib.rs +++ b/protocols/request-response/src/lib.rs @@ -792,9 +792,8 @@ where _addresses: &[Multiaddr], _effective_role: Endpoint, ) -> Result, ConnectionDenied> { - let peer = match maybe_peer { - None => return Ok(vec![]), - Some(peer) => peer, + let Some(peer) = maybe_peer else { + return Ok(vec![]); }; let mut addresses = Vec::new(); diff --git a/protocols/request-response/tests/error_reporting.rs b/protocols/request-response/tests/error_reporting.rs index 60bc5e267e9..79eaaa60366 100644 --- a/protocols/request-response/tests/error_reporting.rs +++ b/protocols/request-response/tests/error_reporting.rs @@ -520,8 +520,9 @@ fn new_swarm_with_config( ) -> (PeerId, Swarm>) { let protocols = iter::once((StreamProtocol::new("/test/1"), ProtocolSupport::Full)); - let swarm = - Swarm::new_ephemeral(|_| request_response::Behaviour::::new(protocols, cfg)); + let swarm = Swarm::new_ephemeral_tokio(|_| { + request_response::Behaviour::::new(protocols, cfg) + }); let peed_id = *swarm.local_peer_id(); (peed_id, swarm) diff --git a/protocols/request-response/tests/peer_address.rs b/protocols/request-response/tests/peer_address.rs index cbdef79b9df..d2ed728559b 100644 --- a/protocols/request-response/tests/peer_address.rs +++ b/protocols/request-response/tests/peer_address.rs @@ -18,11 +18,11 @@ async fn dial_succeeds_after_adding_peers_address() { let protocols = iter::once((StreamProtocol::new("/ping/1"), ProtocolSupport::Full)); let config = request_response::Config::default(); - let mut swarm = Swarm::new_ephemeral(|_| { + let mut swarm = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols.clone(), config.clone()) }); - let mut swarm2 = Swarm::new_ephemeral(|_| { + let mut swarm2 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols.clone(), config.clone()) }); diff --git a/protocols/request-response/tests/ping.rs b/protocols/request-response/tests/ping.rs index ab7409172ad..ee7361af5b7 100644 --- a/protocols/request-response/tests/ping.rs +++ b/protocols/request-response/tests/ping.rs @@ -41,7 +41,7 @@ async fn is_response_outbound() { let ping = Ping("ping".to_string().into_bytes()); let offline_peer = PeerId::random(); - let mut swarm1 = Swarm::new_ephemeral(|_| { + let mut swarm1 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new( [(StreamProtocol::new("/ping/1"), ProtocolSupport::Full)], request_response::Config::default(), @@ -90,11 +90,11 @@ async fn ping_protocol() { let protocols = iter::once((StreamProtocol::new("/ping/1"), ProtocolSupport::Full)); let cfg = request_response::Config::default(); - let mut swarm1 = Swarm::new_ephemeral(|_| { + let mut swarm1 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols.clone(), cfg.clone()) }); let peer1_id = *swarm1.local_peer_id(); - let mut swarm2 = Swarm::new_ephemeral(|_| { + let mut swarm2 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols, cfg) }); let peer2_id = *swarm2.local_peer_id(); @@ -187,11 +187,11 @@ async fn ping_protocol_explicit_address() { let protocols = iter::once((StreamProtocol::new("/ping/1"), ProtocolSupport::Full)); let cfg = request_response::Config::default(); - let mut swarm1 = Swarm::new_ephemeral(|_| { + let mut swarm1 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols.clone(), cfg.clone()) }); let peer1_id = *swarm1.local_peer_id(); - let mut swarm2 = Swarm::new_ephemeral(|_| { + let mut swarm2 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols, cfg) }); let peer2_id = *swarm2.local_peer_id(); @@ -305,11 +305,11 @@ async fn emits_inbound_connection_closed_failure() { let protocols = iter::once((StreamProtocol::new("/ping/1"), ProtocolSupport::Full)); let cfg = request_response::Config::default(); - let mut swarm1 = Swarm::new_ephemeral(|_| { + let mut swarm1 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols.clone(), cfg.clone()) }); let peer1_id = *swarm1.local_peer_id(); - let mut swarm2 = Swarm::new_ephemeral(|_| { + let mut swarm2 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols, cfg) }); let peer2_id = *swarm2.local_peer_id(); @@ -371,11 +371,11 @@ async fn emits_inbound_connection_closed_if_channel_is_dropped() { let protocols = iter::once((StreamProtocol::new("/ping/1"), ProtocolSupport::Full)); let cfg = request_response::Config::default(); - let mut swarm1 = Swarm::new_ephemeral(|_| { + let mut swarm1 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols.clone(), cfg.clone()) }); let peer1_id = *swarm1.local_peer_id(); - let mut swarm2 = Swarm::new_ephemeral(|_| { + let mut swarm2 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols, cfg) }); let peer2_id = *swarm2.local_peer_id(); @@ -432,11 +432,11 @@ async fn concurrent_ping_protocol() { let protocols = iter::once((StreamProtocol::new("/ping/1"), ProtocolSupport::Full)); let cfg = request_response::Config::default(); - let mut swarm1 = Swarm::new_ephemeral(|_| { + let mut swarm1 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols.clone(), cfg.clone()) }); let peer1_id = *swarm1.local_peer_id(); - let mut swarm2 = Swarm::new_ephemeral(|_| { + let mut swarm2 = Swarm::new_ephemeral_tokio(|_| { request_response::cbor::Behaviour::::new(protocols, cfg) }); let peer2_id = *swarm2.local_peer_id(); diff --git a/protocols/stream/CHANGELOG.md b/protocols/stream/CHANGELOG.md index 3c2e8a8a792..43bd94d8096 100644 --- a/protocols/stream/CHANGELOG.md +++ b/protocols/stream/CHANGELOG.md @@ -1,8 +1,9 @@ -## 0.3.0-alpha.1 +## 0.4.0-alpha - Garbage-collect deregistered streams when accepting new streams. See [PR 5999](https://github.com/libp2p/rust-libp2p/pull/5999). + ## 0.3.0-alpha diff --git a/protocols/stream/Cargo.toml b/protocols/stream/Cargo.toml index d6abdcc6a16..9134bae385a 100644 --- a/protocols/stream/Cargo.toml +++ b/protocols/stream/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libp2p-stream" -version = "0.3.0-alpha.1" +version = "0.4.0-alpha" edition.workspace = true rust-version.workspace = true description = "Generic stream protocols for libp2p" diff --git a/protocols/upnp/CHANGELOG.md b/protocols/upnp/CHANGELOG.md index d4aacbe881e..ed693bcf93f 100644 --- a/protocols/upnp/CHANGELOG.md +++ b/protocols/upnp/CHANGELOG.md @@ -1,4 +1,24 @@ -## 0.4.1 +## 0.6.0 + +- Change `Event::NewExternalAddr` and `Event::ExpiredExternalAddr` from tuple variants to struct variants + that include both local and external addresses. This allows users to correlate which local listen + address was mapped to which external address. + - `Event::NewExternalAddr` now contains `local_addr` and `external_addr` fields + - `Event::ExpiredExternalAddr` now contains `local_addr` and `external_addr` fields + See [PR 6121](https://github.com/libp2p/rust-libp2p/pull/6121). + +- Skip port mapping when an active port mapping is present. + Previously, the behavior would skip creating new mappings if any mapping + (active or inactive or pending) existed for the same port. Now it correctly only + checks active mappings on the gateway. + See [PR 6127](https://github.com/libp2p/rust-libp2p/pull/6127). + +- Fix excessive retry attempts for failed port mappings by implementing exponential backoff. + Failed mappings now retry up to 5 times with increasing delays (30s to 480s) before giving up. + This prevents continuous retry loops. + See [PR 6128](https://github.com/libp2p/rust-libp2p/pull/6128). + +## 0.5.0 - update igd-next to 0.16.1 See [PR 5944](https://github.com/libp2p/rust-libp2p/pull/5944). @@ -6,6 +26,8 @@ - Fix panic during a shutdown process. See [PR 5998](https://github.com/libp2p/rust-libp2p/pull/5998). + + ## 0.4.0 - update igd-next to 0.15.1. diff --git a/protocols/upnp/Cargo.toml b/protocols/upnp/Cargo.toml index 2cf121608fc..fb120cfff71 100644 --- a/protocols/upnp/Cargo.toml +++ b/protocols/upnp/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-upnp" edition.workspace = true rust-version.workspace = true description = "UPnP support for libp2p transports" -version = "0.4.1" +version = "0.6.0" license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" keywords = ["peer-to-peer", "libp2p", "networking"] diff --git a/protocols/upnp/src/behaviour.rs b/protocols/upnp/src/behaviour.rs index 6f66c949e82..157cd01e06c 100644 --- a/protocols/upnp/src/behaviour.rs +++ b/protocols/upnp/src/behaviour.rs @@ -53,6 +53,15 @@ const MAPPING_DURATION: u32 = 3600; /// Renew the Mapping every half of `MAPPING_DURATION` to avoid the port being unmapped. const MAPPING_TIMEOUT: u64 = MAPPING_DURATION as u64 / 2; +/// Maximum number of retry attempts for failed mappings. +const MAX_RETRY_ATTEMPTS: u32 = 5; + +/// Base delay in seconds for exponential backoff (will be multiplied by 2^retry_count). +const BASE_RETRY_DELAY_SECS: u64 = 30; + +/// Maximum delay in seconds between retry attempts. +const MAX_RETRY_DELAY_SECS: u64 = 1800; + /// A [`Gateway`] Request. #[derive(Debug)] pub(crate) enum GatewayRequest { @@ -122,11 +131,19 @@ enum MappingState { /// Port mapping is inactive, will be requested or re-requested on the next iteration. Inactive, /// Port mapping/removal has been requested on the gateway. - Pending, + Pending { + /// Number of times we've tried to map this port. + retry_count: u32, + }, /// Port mapping is active with the inner timeout. Active(Delay), - /// Port mapping failed, we will try again. - Failed, + /// Port mapping failed with retry information. + Failed { + /// Number of times we've tried to map this port. + retry_count: u32, + /// When we should try again (None means no more retries). + next_retry: Option, + }, } /// Current state of the UPnP [`Gateway`]. @@ -141,9 +158,19 @@ enum GatewayState { #[derive(Debug)] pub enum Event { /// The multiaddress is reachable externally. - NewExternalAddr(Multiaddr), + NewExternalAddr { + /// The local listen address that was mapped. + local_addr: Multiaddr, + /// The external address that is reachable. + external_addr: Multiaddr, + }, /// The renewal of the multiaddress on the gateway failed. - ExpiredExternalAddr(Multiaddr), + ExpiredExternalAddr { + /// The local listen address that failed to renew. + local_addr: Multiaddr, + /// The external address that is no longer reachable. + external_addr: Multiaddr, + }, /// The IGD gateway was not found. GatewayNotFound, /// The Gateway is not exposed directly to the public network. @@ -174,7 +201,7 @@ impl MappingList { fn renew(&mut self, gateway: &mut Gateway, cx: &mut Context<'_>) { for (mapping, state) in self.iter_mut() { match state { - MappingState::Inactive | MappingState::Failed => { + MappingState::Inactive => { let duration = MAPPING_DURATION; if let Err(err) = gateway.sender.try_send(GatewayRequest::AddMapping { mapping: mapping.clone(), @@ -185,8 +212,34 @@ impl MappingList { "could not request port mapping for multiaddress on the gateway: {}", err ); + } else { + *state = MappingState::Pending { retry_count: 0 }; + } + } + MappingState::Failed { + retry_count, + next_retry, + } => { + if let Some(delay) = next_retry { + if Pin::new(delay).poll(cx).is_ready() { + let duration = MAPPING_DURATION; + if let Err(err) = gateway.sender.try_send(GatewayRequest::AddMapping { + mapping: mapping.clone(), + duration, + }) { + tracing::debug!( + multiaddress=%mapping.multiaddr, + retry_count=%retry_count, + "could not retry port mapping for multiaddress on the gateway: {}", + err + ); + } else { + *state = MappingState::Pending { + retry_count: *retry_count, + }; + } + } } - *state = MappingState::Pending; } MappingState::Active(timeout) => { if Pin::new(timeout).poll(cx).is_ready() { @@ -203,7 +256,7 @@ impl MappingList { } } } - MappingState::Pending => {} + MappingState::Pending { .. } => {} } } } @@ -270,15 +323,14 @@ impl NetworkBehaviour for Behaviour { return; }; - if let Some((mapping, _state)) = self - .mappings - .iter() - .find(|(mapping, _state)| mapping.internal_addr.port() == addr.port()) - { + if let Some((mapping, _state)) = self.mappings.iter().find(|(mapping, state)| { + matches!(state, MappingState::Active(_)) + && mapping.internal_addr.port() == addr.port() + }) { tracing::debug!( multiaddress=%multiaddr, mapped_multiaddress=%mapping.multiaddr, - "port from multiaddress is already being mapped" + "port from multiaddress is already mapped on the gateway" ); return; } @@ -318,7 +370,8 @@ impl NetworkBehaviour for Behaviour { ); } - self.mappings.insert(mapping, MappingState::Pending); + self.mappings + .insert(mapping, MappingState::Pending { retry_count: 0 }); } GatewayState::GatewayNotFound => { tracing::debug!( @@ -352,7 +405,8 @@ impl NetworkBehaviour for Behaviour { err ); } - self.mappings.insert(mapping, MappingState::Pending); + self.mappings + .insert(mapping, MappingState::Pending { retry_count: 0 }); } } } @@ -428,12 +482,13 @@ impl NetworkBehaviour for Behaviour { .insert(mapping.clone(), new_state) .expect("mapping should exist") { - MappingState::Pending => { + MappingState::Pending { .. } => { let external_multiaddr = mapping.external_addr(gateway.external_addr); - self.pending_events.push_back(Event::NewExternalAddr( - external_multiaddr.clone(), - )); + self.pending_events.push_back(Event::NewExternalAddr { + local_addr: mapping.multiaddr, + external_addr: external_multiaddr.clone(), + }); tracing::debug!( address=%mapping.internal_addr, protocol=%mapping.protocol, @@ -454,36 +509,65 @@ impl NetworkBehaviour for Behaviour { } } GatewayEvent::MapFailure(mapping, err) => { - match self - .mappings - .insert(mapping.clone(), MappingState::Failed) - .expect("mapping should exist") - { - MappingState::Active(_) => { - tracing::debug!( - address=%mapping.internal_addr, - protocol=%mapping.protocol, - "failed to remap UPnP mapped for protocol: {err}" - ); - let external_multiaddr = - mapping.external_addr(gateway.external_addr); - self.pending_events.push_back(Event::ExpiredExternalAddr( - external_multiaddr.clone(), - )); - return Poll::Ready(ToSwarm::ExternalAddrExpired( - external_multiaddr, - )); - } - MappingState::Pending => { - tracing::debug!( - address=%mapping.internal_addr, - protocol=%mapping.protocol, - "failed to map UPnP mapped for protocol: {err}" - ); - } - _ => { - unreachable!() + let prev_state = + self.mappings.get(&mapping).expect("mapping should exist"); + + let (retry_count, was_active) = match prev_state { + MappingState::Active(_) => (0, true), + MappingState::Pending { retry_count } => (*retry_count, false), + MappingState::Failed { retry_count, .. } => { + (*retry_count, false) } + _ => unreachable!(), + }; + + let new_retry_count = retry_count + 1; + let next_retry = if new_retry_count < MAX_RETRY_ATTEMPTS { + let delay_secs = std::cmp::min( + BASE_RETRY_DELAY_SECS + .saturating_mul(2_u64.pow(retry_count)), + MAX_RETRY_DELAY_SECS, + ); + Some(Delay::new(Duration::from_secs(delay_secs))) + } else { + tracing::warn!( + address=%mapping.internal_addr, + protocol=%mapping.protocol, + "giving up on UPnP mapping after {new_retry_count} attempts" + ); + None + }; + + self.mappings.insert( + mapping.clone(), + MappingState::Failed { + retry_count: new_retry_count, + next_retry, + }, + ); + + if was_active { + tracing::debug!( + address=%mapping.internal_addr, + protocol=%mapping.protocol, + "failed to remap UPnP mapped for protocol: {err}" + ); + let external_multiaddr = + mapping.external_addr(gateway.external_addr); + self.pending_events.push_back(Event::ExpiredExternalAddr { + local_addr: mapping.multiaddr, + external_addr: external_multiaddr.clone(), + }); + return Poll::Ready(ToSwarm::ExternalAddrExpired( + external_multiaddr, + )); + } else { + tracing::debug!( + address=%mapping.internal_addr, + protocol=%mapping.protocol, + retry_count=%new_retry_count, + "failed to map UPnP mapped for protocol: {err}" + ); } } GatewayEvent::Removed(mapping) => { diff --git a/scripts/ensure-version-bump-and-changelog.sh b/scripts/ensure-version-bump-and-changelog.sh index b4ffeeddb1e..44a49789484 100755 --- a/scripts/ensure-version-bump-and-changelog.sh +++ b/scripts/ensure-version-bump-and-changelog.sh @@ -10,7 +10,7 @@ MERGE_BASE=$(git merge-base "$HEAD_SHA" "$PR_BASE") # Find the merge base. This SRC_DIFF_TO_BASE=$(git diff "$HEAD_SHA".."$MERGE_BASE" --name-status -- "$DIR_TO_CRATE/src" "$DIR_TO_CRATE/Cargo.toml") CHANGELOG_DIFF=$(git diff "$HEAD_SHA".."$MERGE_BASE" --name-only -- "$DIR_TO_CRATE/CHANGELOG.md") -RELEASED_VERSION=$(git tag --sort=version:refname | grep "^$CRATE-v" | tail -n1 | grep -Po "\d+\.\d+\.\d+(-.+)?") +RELEASED_VERSION=$(curl -s -A "Github Action" https://crates.io/api/v1/crates/$CRATE | jq -r .crate.newest_version) # If the source files of this crate weren't touched in this PR, exit early. diff --git a/swarm-test/CHANGELOG.md b/swarm-test/CHANGELOG.md index b82ac708ad5..0198f49b5cf 100644 --- a/swarm-test/CHANGELOG.md +++ b/swarm-test/CHANGELOG.md @@ -2,6 +2,11 @@ - Default to `tokio` runtime. See [PR 6024](https://github.com/libp2p/rust-libp2p/pull/6024). +- Remove `async_std` runtime support with `Swarm::new_ephemeral`. + Use `Swarm::new_ephemeral_tokio` instead. + See [PR 6064](https://github.com/libp2p/rust-libp2p/pull/6064) + + ## 0.5.0 diff --git a/swarm-test/Cargo.toml b/swarm-test/Cargo.toml index bb5b01a990b..7acf6f12a79 100644 --- a/swarm-test/Cargo.toml +++ b/swarm-test/Cargo.toml @@ -26,7 +26,6 @@ futures-timer = "3.0.3" [features] default = ["tokio"] -async-std = ["libp2p-swarm/async-std", "libp2p-tcp/async-io"] tokio = ["libp2p-swarm/tokio", "libp2p-tcp/tokio"] [lints] diff --git a/swarm-test/src/lib.rs b/swarm-test/src/lib.rs index 0bc417dd8b1..9a76ff1f225 100644 --- a/swarm-test/src/lib.rs +++ b/swarm-test/src/lib.rs @@ -38,17 +38,6 @@ use libp2p_swarm::{ pub trait SwarmExt { type NB: NetworkBehaviour; - /// Create a new [`Swarm`] with an ephemeral identity and the `async-std` runtime. - /// - /// The swarm will use a [`libp2p_core::transport::MemoryTransport`] together with a - /// [`libp2p_plaintext::Config`] authentication layer and [`libp2p_yamux::Config`] as the - /// multiplexer. However, these details should not be relied - /// upon by the test and may change at any time. - #[cfg(feature = "async-std")] - fn new_ephemeral(behaviour_fn: impl FnOnce(libp2p_identity::Keypair) -> Self::NB) -> Self - where - Self: Sized; - /// Create a new [`Swarm`] with an ephemeral identity and the `tokio` runtime. /// /// The swarm will use a [`libp2p_core::transport::MemoryTransport`] together with a @@ -223,33 +212,6 @@ where { type NB = B; - #[cfg(feature = "async-std")] - fn new_ephemeral(behaviour_fn: impl FnOnce(libp2p_identity::Keypair) -> Self::NB) -> Self - where - Self: Sized, - { - use libp2p_core::{transport::MemoryTransport, upgrade::Version, Transport as _}; - use libp2p_identity::Keypair; - - let identity = Keypair::generate_ed25519(); - let peer_id = PeerId::from(identity.public()); - - let transport = MemoryTransport::default() - .or_transport(libp2p_tcp::async_io::Transport::default()) - .upgrade(Version::V1) - .authenticate(libp2p_plaintext::Config::new(&identity)) - .multiplex(libp2p_yamux::Config::default()) - .timeout(Duration::from_secs(20)) - .boxed(); - - Swarm::new( - transport, - behaviour_fn(identity), - peer_id, - libp2p_swarm::Config::with_async_std_executor(), - ) - } - #[cfg(feature = "tokio")] fn new_ephemeral_tokio(behaviour_fn: impl FnOnce(libp2p_identity::Keypair) -> Self::NB) -> Self where diff --git a/swarm/CHANGELOG.md b/swarm/CHANGELOG.md index 7415d7d5b38..90d104156f6 100644 --- a/swarm/CHANGELOG.md +++ b/swarm/CHANGELOG.md @@ -1,4 +1,8 @@ ## 0.47.0 + +- Remove `async-std` support. + See [PR 6074](https://github.com/libp2p/rust-libp2p/pull/6074) + - Remove `once_cell` dependency. See [PR 5913](https://github.com/libp2p/rust-libp2p/pull/5913) diff --git a/swarm/Cargo.toml b/swarm/Cargo.toml index 0d4bd83f6cb..90bfa2748d6 100644 --- a/swarm/Cargo.toml +++ b/swarm/Cargo.toml @@ -17,10 +17,11 @@ futures = { workspace = true } futures-timer = "3.0.3" getrandom = { workspace = true, features = ["js"], optional = true } # Explicit dependency to be used in `wasm-bindgen` feature web-time = { workspace = true } +hashlink = { workspace = true } libp2p-core = { workspace = true } -libp2p-identity = { workspace = true } +#libp2p-identity = { workspace = true } +libp2p-identity = { path = "../identity" } libp2p-swarm-derive = { workspace = true, optional = true } -lru = "0.12.3" multistream-select = { workspace = true } rand = "0.8" smallvec = "1.13.2" @@ -28,20 +29,19 @@ tracing = { workspace = true } wasm-bindgen-futures = { version = "0.4.42", optional = true } [target.'cfg(not(any(target_os = "emscripten", target_os = "wasi", target_os = "unknown")))'.dependencies] -async-std = { version = "1.6.2", optional = true } tokio = { workspace = true, features = ["rt"], optional = true } [features] macros = ["dep:libp2p-swarm-derive"] tokio = ["dep:tokio"] -async-std = ["dep:async-std"] wasm-bindgen = ["dep:wasm-bindgen-futures", "dep:getrandom"] [dev-dependencies] either = "1.11.0" futures = { workspace = true } libp2p-identify = { path = "../protocols/identify" } # Using `path` here because this is a cyclic dev-dependency which otherwise breaks releasing. -libp2p-identity = { workspace = true, features = ["ed25519"] } +#libp2p-identity = { workspace = true, features = ["ed25519"] } +libp2p-identity = { path = "../identity", features = ["ed25519"] } libp2p-kad = { path = "../protocols/kad" } # Using `path` here because this is a cyclic dev-dependency which otherwise breaks releasing. libp2p-ping = { path = "../protocols/ping" } # Using `path` here because this is a cyclic dev-dependency which otherwise breaks releasing. libp2p-plaintext = { path = "../transports/plaintext" } # Using `path` here because this is a cyclic dev-dependency which otherwise breaks releasing. diff --git a/swarm/benches/connection_handler.rs b/swarm/benches/connection_handler.rs index 3ae75288208..e1f4b82ae60 100644 --- a/swarm/benches/connection_handler.rs +++ b/swarm/benches/connection_handler.rs @@ -1,7 +1,7 @@ use std::{convert::Infallible, sync::atomic::AtomicUsize}; -use async_std::stream::StreamExt; use criterion::{criterion_group, criterion_main, Criterion}; +use futures::stream::StreamExt; use libp2p_core::{ transport::MemoryTransport, InboundUpgrade, Multiaddr, OutboundUpgrade, Transport, UpgradeInfo, }; diff --git a/swarm/src/behaviour/external_addresses.rs b/swarm/src/behaviour/external_addresses.rs index 6aac4d18e85..30bf2515775 100644 --- a/swarm/src/behaviour/external_addresses.rs +++ b/swarm/src/behaviour/external_addresses.rs @@ -60,13 +60,12 @@ impl ExternalAddresses { FromSwarm::ExternalAddrExpired(ExternalAddrExpired { addr: expired_addr, .. }) => { - let pos = match self + let Some(pos) = self .addresses .iter() .position(|candidate| candidate == *expired_addr) - { - None => return false, - Some(p) => p, + else { + return false; }; self.addresses.remove(pos); diff --git a/swarm/src/behaviour/peer_addresses.rs b/swarm/src/behaviour/peer_addresses.rs index c2d0652ba49..d6aff6e8c42 100644 --- a/swarm/src/behaviour/peer_addresses.rs +++ b/swarm/src/behaviour/peer_addresses.rs @@ -1,8 +1,8 @@ use std::num::NonZeroUsize; +use hashlink::LruCache; use libp2p_core::Multiaddr; use libp2p_identity::PeerId; -use lru::LruCache; use crate::{behaviour::FromSwarm, DialError, DialFailure, NewExternalAddrOfPeer}; @@ -15,7 +15,7 @@ impl PeerAddresses { /// /// For each peer, we will at most store 10 addresses. pub fn new(number_of_peers: NonZeroUsize) -> Self { - Self(LruCache::new(number_of_peers)) + Self(LruCache::new(number_of_peers.get())) } /// Feed a [`FromSwarm`] event to this struct. @@ -48,11 +48,11 @@ impl PeerAddresses { match prepare_addr(&peer, &address) { Ok(address) => { if let Some(cached) = self.0.get_mut(&peer) { - cached.put(address, ()).is_none() + cached.insert(address, ()).is_none() } else { - let mut set = LruCache::new(NonZeroUsize::new(10).expect("10 > 0")); - set.put(address, ()); - self.0.put(peer, set); + let mut set = LruCache::new(10); + set.insert(address, ()); + self.0.insert(peer, set); true } @@ -75,7 +75,7 @@ impl PeerAddresses { pub fn remove(&mut self, peer: &PeerId, address: &Multiaddr) -> bool { match self.0.get_mut(peer) { Some(addrs) => match prepare_addr(peer, address) { - Ok(address) => addrs.pop(&address).is_some(), + Ok(address) => addrs.remove(&address).is_some(), Err(_) => false, }, None => false, @@ -89,7 +89,7 @@ fn prepare_addr(peer: &PeerId, addr: &Multiaddr) -> Result impl Default for PeerAddresses { fn default() -> Self { - Self(LruCache::new(NonZeroUsize::new(100).unwrap())) + Self(LruCache::new(100)) } } diff --git a/swarm/src/behaviour/toggle.rs b/swarm/src/behaviour/toggle.rs index 03cf062416f..d029926fd8f 100644 --- a/swarm/src/behaviour/toggle.rs +++ b/swarm/src/behaviour/toggle.rs @@ -80,9 +80,8 @@ where local_addr: &Multiaddr, remote_addr: &Multiaddr, ) -> Result<(), ConnectionDenied> { - let inner = match self.inner.as_mut() { - None => return Ok(()), - Some(inner) => inner, + let Some(inner) = self.inner.as_mut() else { + return Ok(()); }; inner.handle_pending_inbound_connection(connection_id, local_addr, remote_addr)?; @@ -97,9 +96,8 @@ where local_addr: &Multiaddr, remote_addr: &Multiaddr, ) -> Result, ConnectionDenied> { - let inner = match self.inner.as_mut() { - None => return Ok(ToggleConnectionHandler { inner: None }), - Some(inner) => inner, + let Some(inner) = self.inner.as_mut() else { + return Ok(ToggleConnectionHandler { inner: None }); }; let handler = inner.handle_established_inbound_connection( @@ -121,9 +119,8 @@ where addresses: &[Multiaddr], effective_role: Endpoint, ) -> Result, ConnectionDenied> { - let inner = match self.inner.as_mut() { - None => return Ok(vec![]), - Some(inner) => inner, + let Some(inner) = self.inner.as_mut() else { + return Ok(vec![]); }; let addresses = inner.handle_pending_outbound_connection( @@ -144,9 +141,8 @@ where role_override: Endpoint, port_use: PortUse, ) -> Result, ConnectionDenied> { - let inner = match self.inner.as_mut() { - None => return Ok(ToggleConnectionHandler { inner: None }), - Some(inner) => inner, + let Some(inner) = self.inner.as_mut() else { + return Ok(ToggleConnectionHandler { inner: None }); }; let handler = inner.handle_established_outbound_connection( diff --git a/swarm/src/connection.rs b/swarm/src/connection.rs index ff2de876007..e52d4b0e893 100644 --- a/swarm/src/connection.rs +++ b/swarm/src/connection.rs @@ -760,7 +760,7 @@ enum Shutdown { Later(Delay), } -// Structure used to avoid allocations when storing the protocols in the `HashMap. +// Structure used to avoid allocations when storing the protocols in the `HashMap`. // Instead of allocating a new `String` for the key, // we use `T::as_ref()` in `Hash`, `Eq` and `PartialEq` requirements. pub(crate) struct AsStrHashEq(pub(crate) T); diff --git a/swarm/src/executor.rs b/swarm/src/executor.rs index db5ed6b2da4..41fb37aaf15 100644 --- a/swarm/src/executor.rs +++ b/swarm/src/executor.rs @@ -44,22 +44,6 @@ impl Executor for TokioExecutor { } } -#[cfg(all( - feature = "async-std", - not(any(target_os = "emscripten", target_os = "wasi", target_os = "unknown")) -))] -#[derive(Default, Debug, Clone, Copy)] -pub(crate) struct AsyncStdExecutor; -#[cfg(all( - feature = "async-std", - not(any(target_os = "emscripten", target_os = "wasi", target_os = "unknown")) -))] -impl Executor for AsyncStdExecutor { - fn exec(&self, future: Pin + Send>>) { - async_std::task::spawn(future); - } -} - #[cfg(feature = "wasm-bindgen")] #[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct WasmBindgenExecutor; diff --git a/swarm/src/handler.rs b/swarm/src/handler.rs index 0e4d1f5a325..633f75304c0 100644 --- a/swarm/src/handler.rs +++ b/swarm/src/handler.rs @@ -171,10 +171,10 @@ pub trait ConnectionHandler: Send + 'static { /// This is also called when the shutdown was initiated due to an error on the connection. /// We therefore cannot guarantee that performing IO within here will succeed. /// - /// To signal completion, [`Poll::Ready(None)`] should be returned. + /// To signal completion, [`Poll::Ready(None)`](Poll::Ready) should be returned. /// /// Implementations MUST have a [`fuse`](futures::StreamExt::fuse)-like behaviour. - /// That is, [`Poll::Ready(None)`] MUST be returned on repeated calls to + /// That is, [`Poll::Ready(None)`](Poll::Ready) MUST be returned on repeated calls to /// [`ConnectionHandler::poll_close`]. fn poll_close(&mut self, _: &mut Context<'_>) -> Poll> { Poll::Ready(None) diff --git a/swarm/src/lib.rs b/swarm/src/lib.rs index 6dcd9a63590..d0ae6118190 100644 --- a/swarm/src/lib.rs +++ b/swarm/src/lib.rs @@ -1420,15 +1420,6 @@ impl Config { Self::with_executor(crate::executor::TokioExecutor) } - /// Builds a new [`Config`] from the given `async-std` executor. - #[cfg(all( - feature = "async-std", - not(any(target_os = "emscripten", target_os = "wasi", target_os = "unknown")) - ))] - pub fn with_async_std_executor() -> Self { - Self::with_executor(crate::executor::AsyncStdExecutor) - } - /// Configures the number of events from the [`NetworkBehaviour`] in /// destination to the [`ConnectionHandler`] that can be buffered before /// the [`Swarm`] has to wait. An individual buffer with this number of diff --git a/swarm/src/test.rs b/swarm/src/test.rs index 250960fbcca..efbaddfb0c6 100644 --- a/swarm/src/test.rs +++ b/swarm/src/test.rs @@ -113,9 +113,8 @@ where _addresses: &[Multiaddr], _effective_role: Endpoint, ) -> Result, ConnectionDenied> { - let p = match maybe_peer { - None => return Ok(vec![]), - Some(peer) => peer, + let Some(p) = maybe_peer else { + return Ok(vec![]); }; Ok(self.addresses.get(&p).map_or(Vec::new(), |v| v.clone())) diff --git a/swarm/tests/swarm_derive.rs b/swarm/tests/swarm_derive.rs index e297f2f4afa..0906aa80723 100644 --- a/swarm/tests/swarm_derive.rs +++ b/swarm/tests/swarm_derive.rs @@ -84,8 +84,8 @@ fn two_fields() { let _out_event: ::ToSwarm = unimplemented!(); match _out_event { FooEvent::Ping(ping::Event { .. }) => {} - FooEvent::Identify(event) => { - let _: identify::Event = event; + FooEvent::Identify(_event) => { + let _: identify::Event = _event; } } } @@ -112,11 +112,11 @@ fn three_fields() { let _out_event: ::ToSwarm = unimplemented!(); match _out_event { FooEvent::Ping(ping::Event { .. }) => {} - FooEvent::Identify(event) => { - let _: identify::Event = event; + FooEvent::Identify(_event) => { + let _: identify::Event = _event; } - FooEvent::Kad(event) => { - let _: libp2p_kad::Event = event; + FooEvent::Kad(_event) => { + let _: libp2p_kad::Event = _event; } } } diff --git a/transports/dns/CHANGELOG.md b/transports/dns/CHANGELOG.md index e219a37b12f..b0c17e373c1 100644 --- a/transports/dns/CHANGELOG.md +++ b/transports/dns/CHANGELOG.md @@ -1,8 +1,10 @@ ## 0.44.0 - -- Report all transport errors in a dial attempt instead of only returning the last error. + +- Removed `async_std` module [PR 5959](https://github.com/libp2p/rust-libp2p/pull/5959) + +- Report all transport errors in a dial attempt instead of only returning the last error. See [PR 5899](https://github.com/libp2p/rust-libp2p/pull/5899). - + ## 0.43.0 - Upgrade `async-std-resolver` and `hickory-resolver`. diff --git a/transports/dns/Cargo.toml b/transports/dns/Cargo.toml index 840e507d520..6cfc1e93f08 100644 --- a/transports/dns/Cargo.toml +++ b/transports/dns/Cargo.toml @@ -11,7 +11,6 @@ keywords = ["peer-to-peer", "libp2p", "networking"] categories = ["network-programming", "asynchronous"] [dependencies] -async-std-resolver = { workspace = true, features = ["system-config"], optional = true } async-trait = "0.1.80" futures = { workspace = true } libp2p-core = { workspace = true } @@ -24,12 +23,10 @@ tracing = { workspace = true } [dev-dependencies] libp2p-identity = { workspace = true, features = ["rand"] } tokio = { workspace = true, features = ["rt", "time"] } -async-std-crate = { package = "async-std", version = "1.6" } tracing-subscriber = { workspace = true, features = ["env-filter"] } [features] -async-std = ["async-std-resolver"] -tokio = ["hickory-resolver/tokio-runtime"] +tokio = ["hickory-resolver/tokio"] # Passing arguments to the docsrs builder in order to properly document cfg's. # More information: https://docs.rs/about/builds#cross-compiling diff --git a/transports/dns/src/lib.rs b/transports/dns/src/lib.rs index f6b79ef4f63..2d9be6e8157 100644 --- a/transports/dns/src/lib.rs +++ b/transports/dns/src/lib.rs @@ -21,9 +21,7 @@ //! # [DNS name resolution](https://github.com/libp2p/specs/blob/master/addressing/README.md#ip-and-name-resolution) //! [`Transport`] for libp2p. //! -//! This crate provides the type [`async_std::Transport`] and [`tokio::Transport`] -//! for use with `async-std` and `tokio`, -//! respectively. +//! This crate provides the type [`tokio::Transport`] based on [`hickory_resolver::TokioResolver`]. //! //! A [`Transport`] is an address-rewriting [`libp2p_core::Transport`] wrapper around //! an inner `Transport`. The composed transport behaves like the inner @@ -31,11 +29,12 @@ //! `/dns6/...` and `/dnsaddr/...` components of the given `Multiaddr` through //! a DNS, replacing them with the resolved protocols (typically TCP/IP). //! -//! The `async-std` feature and hence the [`async_std::Transport`] are -//! enabled by default. Tokio users can furthermore opt-in -//! to the `tokio-dns-over-rustls` and `tokio-dns-over-https-rustls` -//! features. For more information about these features, please -//! refer to the documentation of [trust-dns-resolver]. +//! The [`tokio::Transport`] is enabled by default under the `tokio` feature. +//! Tokio users can furthermore opt-in to the `tokio-dns-over-rustls` and +//! `tokio-dns-over-https-rustls` features. +//! For more information about these features, please refer to the documentation +//! of [trust-dns-resolver]. +//! Alternative runtimes or resolvers can be used though a manual implementation of [`Resolver`]. //! //! On Unix systems, if no custom configuration is given, [trust-dns-resolver] //! will try to parse the `/etc/resolv.conf` file. This approach comes with a @@ -56,68 +55,11 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -#[cfg(feature = "async-std")] -pub mod async_std { - use std::{io, sync::Arc}; - - use async_std_resolver::AsyncStdResolver; - use futures::FutureExt; - use hickory_resolver::{ - config::{ResolverConfig, ResolverOpts}, - system_conf, - }; - use parking_lot::Mutex; - - /// A `Transport` wrapper for performing DNS lookups when dialing `Multiaddr`esses - /// using `async-std` for all async I/O. - pub type Transport = crate::Transport; - - impl Transport { - /// Creates a new [`Transport`] from the OS's DNS configuration and defaults. - pub async fn system(inner: T) -> Result, io::Error> { - let (cfg, opts) = system_conf::read_system_conf()?; - Ok(Self::custom(inner, cfg, opts).await) - } - - /// Creates a [`Transport`] with a custom resolver configuration and options. - pub async fn custom(inner: T, cfg: ResolverConfig, opts: ResolverOpts) -> Transport { - Transport { - inner: Arc::new(Mutex::new(inner)), - resolver: async_std_resolver::resolver(cfg, opts).await, - } - } - - // TODO: Replace `system` implementation with this - #[doc(hidden)] - pub fn system2(inner: T) -> Result, io::Error> { - Ok(Transport { - inner: Arc::new(Mutex::new(inner)), - resolver: async_std_resolver::resolver_from_system_conf() - .now_or_never() - .expect( - "async_std_resolver::resolver_from_system_conf did not resolve immediately", - )?, - }) - } - - // TODO: Replace `custom` implementation with this - #[doc(hidden)] - pub fn custom2(inner: T, cfg: ResolverConfig, opts: ResolverOpts) -> Transport { - Transport { - inner: Arc::new(Mutex::new(inner)), - resolver: async_std_resolver::resolver(cfg, opts) - .now_or_never() - .expect("async_std_resolver::resolver did not resolve immediately"), - } - } - } -} - #[cfg(feature = "tokio")] pub mod tokio { use std::sync::Arc; - use hickory_resolver::{system_conf, TokioResolver}; + use hickory_resolver::{name_server::TokioConnectionProvider, system_conf, TokioResolver}; use parking_lot::Mutex; /// A `Transport` wrapper for performing DNS lookups when dialing `Multiaddr`esses @@ -140,7 +82,12 @@ pub mod tokio { ) -> Transport { Transport { inner: Arc::new(Mutex::new(inner)), - resolver: TokioResolver::tokio(cfg, opts), + resolver: TokioResolver::builder_with_config( + cfg, + TokioConnectionProvider::default(), + ) + .with_options(opts) + .build(), } } } @@ -193,8 +140,7 @@ const MAX_DNS_LOOKUPS: usize = 32; const MAX_TXT_RECORDS: usize = 16; /// A [`Transport`] for performing DNS lookups when dialing `Multiaddr`esses. -/// You shouldn't need to use this type directly. Use [`tokio::Transport`] or -/// [`async_std::Transport`] instead. +/// You shouldn't need to use this type directly. Use [`tokio::Transport`] instead. #[derive(Debug)] pub struct Transport { /// The underlying transport. @@ -625,7 +571,7 @@ where } } -#[cfg(all(test, any(feature = "tokio", feature = "async-std")))] +#[cfg(all(test, feature = "tokio"))] mod tests { use futures::future::BoxFuture; use hickory_resolver::proto::{ProtoError, ProtoErrorKind}; @@ -638,21 +584,6 @@ mod tests { use super::*; - // These helpers will be compiled conditionally, depending on the async runtime in use. - - #[cfg(feature = "async-std")] - fn test_async_std>( - transport: T, - test_fn: impl FnOnce(async_std::Transport) -> F, - ) { - let config = ResolverConfig::quad9(); - let opts = ResolverOpts::default(); - let transport = - async_std_crate::task::block_on(async_std::Transport::custom(transport, config, opts)); - async_std_crate::task::block_on(test_fn(transport)); - } - - #[cfg(feature = "tokio")] fn test_tokio>( transport: T, test_fn: impl FnOnce(tokio::Transport) -> F, @@ -818,15 +749,7 @@ mod tests { } } - #[cfg(feature = "async-std")] - { - test_async_std(CustomTransport, run); - } - - #[cfg(feature = "tokio")] - { - test_tokio(CustomTransport, run); - } + test_tokio(CustomTransport, run); } #[test] @@ -920,10 +843,6 @@ mod tests { } } - #[cfg(feature = "async-std")] - test_async_std(AlwaysFailTransport, run_test); - - #[cfg(feature = "tokio")] test_tokio(AlwaysFailTransport, run_test); } } diff --git a/transports/noise/src/io/framed.rs b/transports/noise/src/io/framed.rs index 099bf7eac03..084474bcb0b 100644 --- a/transports/noise/src/io/framed.rs +++ b/transports/noise/src/io/framed.rs @@ -126,11 +126,11 @@ impl Decoder for Codec { type Item = proto::NoiseHandshakePayload; fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - let cleartext = match decrypt(src, |ciphertext, decrypt_buffer| { + let Some(cleartext) = decrypt(src, |ciphertext, decrypt_buffer| { self.session.read_message(ciphertext, decrypt_buffer) - })? { - None => return Ok(None), - Some(cleartext) => cleartext, + })? + else { + return Ok(None); }; let mut reader = BytesReader::from_bytes(&cleartext[..]); diff --git a/transports/quic/Cargo.toml b/transports/quic/Cargo.toml index 513af521779..402a10564db 100644 --- a/transports/quic/Cargo.toml +++ b/transports/quic/Cargo.toml @@ -21,7 +21,7 @@ rustls = { version = "0.23.9", default-features = false } thiserror = { workspace = true } tokio = { workspace = true, default-features = false, features = ["net", "rt", "time"], optional = true } tracing = { workspace = true } -socket2 = "0.5.7" +socket2 = "0.6.0" ring = { workspace = true } [features] diff --git a/transports/quic/tests/smoke.rs b/transports/quic/tests/smoke.rs index 2f3fe71da9c..b14c17b2564 100644 --- a/transports/quic/tests/smoke.rs +++ b/transports/quic/tests/smoke.rs @@ -41,12 +41,6 @@ async fn tokio_smoke() { smoke::().await } -#[cfg(feature = "tokio")] -#[tokio::test] -async fn async_std_smoke() { - smoke::().await -} - #[cfg(feature = "tokio")] #[tokio::test] async fn endpoint_reuse() { diff --git a/transports/tcp/CHANGELOG.md b/transports/tcp/CHANGELOG.md index 3c4b0bc946d..ff78d9d1c66 100644 --- a/transports/tcp/CHANGELOG.md +++ b/transports/tcp/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.44.1 +- Expose `socket2`'s `set_linger` config option. + See [PR 6225](https://github.com/libp2p/rust-libp2p/pull/5955) + +## 0.44.0 + +- Remove `async-std` support. + See [PR 5955](https://github.com/libp2p/rust-libp2p/pull/5955) + ## 0.43.0 - Fix the disabling of Nagle's algorithm, which requires setting `TCP_NODELAY` to _true_. diff --git a/transports/tcp/Cargo.toml b/transports/tcp/Cargo.toml index ef15e21553c..cbff8517593 100644 --- a/transports/tcp/Cargo.toml +++ b/transports/tcp/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-tcp" edition.workspace = true rust-version = { workspace = true } description = "TCP/IP transport protocol for libp2p" -version = "0.43.0" +version = "0.44.1" authors = ["Parity Technologies "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" @@ -11,22 +11,19 @@ keywords = ["peer-to-peer", "libp2p", "networking"] categories = ["network-programming", "asynchronous"] [dependencies] -async-io = { version = "2.3.3", optional = true } futures = { workspace = true } futures-timer = "3.0" if-watch = { workspace = true } libc = "0.2.155" libp2p-core = { workspace = true } -socket2 = { version = "0.5.7", features = ["all"] } +socket2 = { version = "0.6.0", features = ["all"] } tokio = { workspace = true, default-features = false, features = ["net"], optional = true } tracing = { workspace = true } [features] tokio = ["dep:tokio", "if-watch/tokio"] -async-io = ["dep:async-io", "if-watch/smol"] [dev-dependencies] -async-std = { version = "1.6.5", features = ["attributes"] } tokio = { workspace = true, features = ["full"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/transports/tcp/src/lib.rs b/transports/tcp/src/lib.rs index 7eb25c81e92..f91eee63085 100644 --- a/transports/tcp/src/lib.rs +++ b/transports/tcp/src/lib.rs @@ -22,8 +22,8 @@ //! //! # Usage //! -//! This crate provides a [`async_io::Transport`] and [`tokio::Transport`], depending on -//! the enabled features, which implement the [`libp2p_core::Transport`] trait for use as a +//! This crate provides [`tokio::Transport`] +//! which implement the [`libp2p_core::Transport`] trait for use as a //! transport with `libp2p-core` or `libp2p-swarm`. #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] @@ -47,8 +47,6 @@ use libp2p_core::{ multiaddr::{Multiaddr, Protocol}, transport::{DialOpts, ListenerId, PortUse, TransportError, TransportEvent}, }; -#[cfg(feature = "async-io")] -pub use provider::async_io; #[cfg(feature = "tokio")] pub use provider::tokio; use provider::{Incoming, Provider}; @@ -61,6 +59,9 @@ pub struct Config { ttl: Option, /// `TCP_NODELAY` to set for opened sockets. nodelay: bool, + /// `SO_LINGER` to set for opened sockets, + /// by default is `None`. + linger: Option, /// Size of the listen backlog for listen sockets. backlog: u32, } @@ -139,6 +140,7 @@ impl Config { Self { ttl: None, nodelay: true, // Disable Nagle's algorithm by default. + linger: None, backlog: 1024, } } @@ -161,14 +163,20 @@ impl Config { self } + /// Configures the `SO_LINGER` option for new sockets. + pub fn linger(mut self, duration: Option) -> Self { + self.linger = duration; + self + } + /// Configures port reuse for local sockets, which implies /// reuse of listening ports for outgoing connections to /// enhance NAT traversal capabilities. /// /// # Deprecation Notice /// - /// The new implementation works on a per-connaction basis, defined by the behaviour. This - /// removes the necessaity to configure the transport for port reuse, instead the behaviour + /// The new implementation works on a per-connection basis, defined by the behaviour. This + /// removes the necessity to configure the transport for port reuse, instead the behaviour /// requiring this behaviour can decide whether to use port reuse or not. /// /// The API to configure port reuse is part of [`Transport`] and the option can be found in @@ -177,7 +185,7 @@ impl Config { /// If [`PortUse::Reuse`] is enabled, the transport will try to reuse the local port of the /// listener. If that's not possible, i.e. there is no listener or the transport doesn't allow /// a direct control over ports, a new port (or the default behaviour) is used. If port reuse - /// is enabled for a connection, this option will be treated on a best-effor basis. + /// is enabled for a connection, this option will be treated on a best-effort basis. #[deprecated( since = "0.42.0", note = "This option does nothing now, since the port reuse policy is now decided on a per-connection basis by the behaviour. The function will be removed in a future release." @@ -196,9 +204,14 @@ impl Config { socket.set_only_v6(true)?; } if let Some(ttl) = self.ttl { - socket.set_ttl(ttl)?; + socket.set_ttl_v4(ttl)?; } - socket.set_nodelay(self.nodelay)?; + + if self.linger.is_some() { + socket.set_linger(self.linger)?; + } + + socket.set_tcp_nodelay(self.nodelay)?; socket.set_reuse_address(true)?; #[cfg(all(unix, not(any(target_os = "solaris", target_os = "illumos"))))] if port_use == PortUse::Reuse { @@ -225,7 +238,6 @@ impl Default for Config { /// You shouldn't need to use this type directly. Use one of the following instead: /// /// - [`tokio::Transport`] -/// - [`async_io::Transport`] pub struct Transport where T: Provider + Send, @@ -254,7 +266,6 @@ where /// It is best to call this function through one of the type-aliases of this type: /// /// - [`tokio::Transport::new`] - /// - [`async_io::Transport::new`] pub fn new(config: Config) -> Self { Transport { config, @@ -699,7 +710,7 @@ fn ip_to_multiaddr(ip: IpAddr, port: u16) -> Multiaddr { Multiaddr::empty().with(ip.into()).with(Protocol::Tcp(port)) } -#[cfg(test)] +#[cfg(all(test, feature = "tokio"))] mod tests { use futures::{ channel::{mpsc, oneshot}, @@ -809,30 +820,17 @@ mod tests { } fn test(addr: Multiaddr) { - #[cfg(feature = "async-io")] - { - let (ready_tx, ready_rx) = mpsc::channel(1); - let listener = listener::(addr.clone(), ready_tx); - let dialer = dialer::(ready_rx); - let listener = async_std::task::spawn(listener); - async_std::task::block_on(dialer); - async_std::task::block_on(listener); - } - - #[cfg(feature = "tokio")] - { - let (ready_tx, ready_rx) = mpsc::channel(1); - let listener = listener::(addr, ready_tx); - let dialer = dialer::(ready_rx); - let rt = ::tokio::runtime::Builder::new_current_thread() - .enable_io() - .build() - .unwrap(); - let tasks = ::tokio::task::LocalSet::new(); - let listener = tasks.spawn_local(listener); - tasks.block_on(&rt, dialer); - tasks.block_on(&rt, listener).unwrap(); - } + let (ready_tx, ready_rx) = mpsc::channel(1); + let listener = listener::(addr, ready_tx); + let dialer = dialer::(ready_rx); + let rt = ::tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap(); + let tasks = ::tokio::task::LocalSet::new(); + let listener = tasks.spawn_local(listener); + tasks.block_on(&rt, dialer); + tasks.block_on(&rt, listener).unwrap(); } test("/ip4/127.0.0.1/tcp/0".parse().unwrap()); @@ -889,30 +887,17 @@ mod tests { } fn test(addr: Multiaddr) { - #[cfg(feature = "async-io")] - { - let (ready_tx, ready_rx) = mpsc::channel(1); - let listener = listener::(addr.clone(), ready_tx); - let dialer = dialer::(ready_rx); - let listener = async_std::task::spawn(listener); - async_std::task::block_on(dialer); - async_std::task::block_on(listener); - } - - #[cfg(feature = "tokio")] - { - let (ready_tx, ready_rx) = mpsc::channel(1); - let listener = listener::(addr, ready_tx); - let dialer = dialer::(ready_rx); - let rt = ::tokio::runtime::Builder::new_current_thread() - .enable_io() - .build() - .unwrap(); - let tasks = ::tokio::task::LocalSet::new(); - let listener = tasks.spawn_local(listener); - tasks.block_on(&rt, dialer); - tasks.block_on(&rt, listener).unwrap(); - } + let (ready_tx, ready_rx) = mpsc::channel(1); + let listener = listener::(addr, ready_tx); + let dialer = dialer::(ready_rx); + let rt = ::tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap(); + let tasks = ::tokio::task::LocalSet::new(); + let listener = tasks.spawn_local(listener); + tasks.block_on(&rt, dialer); + tasks.block_on(&rt, listener).unwrap(); } test("/ip4/0.0.0.0/tcp/0".parse().unwrap()); @@ -1006,32 +991,18 @@ mod tests { } fn test(addr: Multiaddr) { - #[cfg(feature = "async-io")] - { - let (ready_tx, ready_rx) = mpsc::channel(1); - let (port_reuse_tx, port_reuse_rx) = oneshot::channel(); - let listener = listener::(addr.clone(), ready_tx, port_reuse_rx); - let dialer = dialer::(addr.clone(), ready_rx, port_reuse_tx); - let listener = async_std::task::spawn(listener); - async_std::task::block_on(dialer); - async_std::task::block_on(listener); - } - - #[cfg(feature = "tokio")] - { - let (ready_tx, ready_rx) = mpsc::channel(1); - let (port_reuse_tx, port_reuse_rx) = oneshot::channel(); - let listener = listener::(addr.clone(), ready_tx, port_reuse_rx); - let dialer = dialer::(addr, ready_rx, port_reuse_tx); - let rt = ::tokio::runtime::Builder::new_current_thread() - .enable_io() - .build() - .unwrap(); - let tasks = ::tokio::task::LocalSet::new(); - let listener = tasks.spawn_local(listener); - tasks.block_on(&rt, dialer); - tasks.block_on(&rt, listener).unwrap(); - } + let (ready_tx, ready_rx) = mpsc::channel(1); + let (port_reuse_tx, port_reuse_rx) = oneshot::channel(); + let listener = listener::(addr.clone(), ready_tx, port_reuse_rx); + let dialer = dialer::(addr, ready_rx, port_reuse_tx); + let rt = ::tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap(); + let tasks = ::tokio::task::LocalSet::new(); + let listener = tasks.spawn_local(listener); + tasks.block_on(&rt, dialer); + tasks.block_on(&rt, listener).unwrap(); } test("/ip4/127.0.0.1/tcp/0".parse().unwrap()); @@ -1074,21 +1045,12 @@ mod tests { } fn test(addr: Multiaddr) { - #[cfg(feature = "async-io")] - { - let listener = listen_twice::(addr.clone()); - async_std::task::block_on(listener); - } - - #[cfg(feature = "tokio")] - { - let listener = listen_twice::(addr); - let rt = ::tokio::runtime::Builder::new_current_thread() - .enable_io() - .build() - .unwrap(); - rt.block_on(listener); - } + let listener = listen_twice::(addr); + let rt = ::tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap(); + rt.block_on(listener); } test("/ip4/127.0.0.1/tcp/0".parse().unwrap()); @@ -1110,21 +1072,12 @@ mod tests { } fn test(addr: Multiaddr) { - #[cfg(feature = "async-io")] - { - let new_addr = async_std::task::block_on(listen::(addr.clone())); - assert!(!new_addr.to_string().contains("tcp/0")); - } - - #[cfg(feature = "tokio")] - { - let rt = ::tokio::runtime::Builder::new_current_thread() - .enable_io() - .build() - .unwrap(); - let new_addr = rt.block_on(listen::(addr)); - assert!(!new_addr.to_string().contains("tcp/0")); - } + let rt = ::tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap(); + let new_addr = rt.block_on(listen::(addr)); + assert!(!new_addr.to_string().contains("tcp/0")); } test("/ip6/::1/tcp/0".parse().unwrap()); @@ -1138,17 +1091,8 @@ mod tests { .try_init(); fn test(addr: Multiaddr) { - #[cfg(feature = "async-io")] - { - let mut tcp = async_io::Transport::default(); - assert!(tcp.listen_on(ListenerId::next(), addr.clone()).is_err()); - } - - #[cfg(feature = "tokio")] - { - let mut tcp = tokio::Transport::default(); - assert!(tcp.listen_on(ListenerId::next(), addr).is_err()); - } + let mut tcp = tokio::Transport::default(); + assert!(tcp.listen_on(ListenerId::next(), addr).is_err()); } test("/ip4/127.0.0.1/tcp/12345/tcp/12345".parse().unwrap()); @@ -1168,11 +1112,6 @@ mod tests { tcp.remove_listener(listener_id) } - #[cfg(feature = "async-io")] - { - assert!(async_std::task::block_on(cycle_listeners::())); - } - #[cfg(feature = "tokio")] { let rt = ::tokio::runtime::Builder::new_current_thread() @@ -1203,12 +1142,6 @@ mod tests { ) .unwrap(); } - #[cfg(feature = "async-io")] - { - async_std::task::block_on(async { - test::(); - }) - } #[cfg(feature = "tokio")] { let rt = ::tokio::runtime::Builder::new_current_thread() diff --git a/transports/tcp/src/provider.rs b/transports/tcp/src/provider.rs index 7a609d9f031..3d9acc76ea7 100644 --- a/transports/tcp/src/provider.rs +++ b/transports/tcp/src/provider.rs @@ -20,9 +20,6 @@ //! The interface for providers of non-blocking TCP implementations. -#[cfg(feature = "async-io")] -pub mod async_io; - #[cfg(feature = "tokio")] pub mod tokio; diff --git a/transports/tcp/src/provider/async_io.rs b/transports/tcp/src/provider/async_io.rs deleted file mode 100644 index 4df9d928fbb..00000000000 --- a/transports/tcp/src/provider/async_io.rs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2020 Parity Technologies (UK) Ltd. -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -use std::{ - io, net, - task::{Context, Poll}, -}; - -use async_io::Async; -use futures::future::{BoxFuture, FutureExt}; - -use super::{Incoming, Provider}; - -/// A TCP [`Transport`](libp2p_core::Transport) that works with the `async-std` ecosystem. -/// -/// # Example -/// -/// ```rust -/// # use libp2p_tcp as tcp; -/// # use libp2p_core::{Transport, transport::ListenerId}; -/// # use futures::future; -/// # use std::pin::Pin; -/// # -/// # #[async_std::main] -/// # async fn main() { -/// let mut transport = tcp::async_io::Transport::new(tcp::Config::default()); -/// let id = ListenerId::next(); -/// transport -/// .listen_on(id, "/ip4/127.0.0.1/tcp/0".parse().unwrap()) -/// .unwrap(); -/// -/// let addr = future::poll_fn(|cx| Pin::new(&mut transport).poll(cx)) -/// .await -/// .into_new_address() -/// .unwrap(); -/// -/// println!("Listening on {addr}"); -/// # } -/// ``` -pub type Transport = crate::Transport; - -#[derive(Copy, Clone)] -#[doc(hidden)] -pub enum Tcp {} - -impl Provider for Tcp { - type Stream = TcpStream; - type Listener = Async; - type IfWatcher = if_watch::smol::IfWatcher; - - fn new_if_watcher() -> io::Result { - Self::IfWatcher::new() - } - - fn addrs(if_watcher: &Self::IfWatcher) -> Vec { - if_watcher.iter().copied().collect() - } - - fn new_listener(l: net::TcpListener) -> io::Result { - Async::new(l) - } - - fn new_stream(s: net::TcpStream) -> BoxFuture<'static, io::Result> { - async move { - // Taken from [`Async::connect`]. - - let stream = Async::new(s)?; - - // The stream becomes writable when connected. - stream.writable().await?; - - // Check if there was an error while connecting. - match stream.get_ref().take_error()? { - None => Ok(stream), - Some(err) => Err(err), - } - } - .boxed() - } - - fn poll_accept( - l: &mut Self::Listener, - cx: &mut Context<'_>, - ) -> Poll>> { - let (stream, remote_addr) = loop { - match l.poll_readable(cx) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Ready(Ok(())) => match l.accept().now_or_never() { - Some(Err(e)) => return Poll::Ready(Err(e)), - Some(Ok(res)) => break res, - None => { - // Since it doesn't do any harm, account for false positives of - // `poll_readable` just in case, i.e. try again. - } - }, - } - }; - - let local_addr = stream.get_ref().local_addr()?; - - Poll::Ready(Ok(Incoming { - stream, - local_addr, - remote_addr, - })) - } -} - -pub type TcpStream = Async; diff --git a/transports/tls/src/certificate.rs b/transports/tls/src/certificate.rs index 4ab22ad54cd..2127746ec2e 100644 --- a/transports/tls/src/certificate.rs +++ b/transports/tls/src/certificate.rs @@ -167,7 +167,7 @@ pub struct VerificationError(#[from] pub(crate) webpki::Error); /// Internal function that only parses but does not verify the certificate. /// /// Useful for testing but unsuitable for production. -fn parse_unverified(der_input: &[u8]) -> Result { +fn parse_unverified(der_input: &[u8]) -> Result, webpki::Error> { let x509 = X509Certificate::from_der(der_input) .map(|(_rest_input, x509)| x509) .map_err(|_| webpki::Error::BadDer)?; @@ -185,7 +185,7 @@ fn parse_unverified(der_input: &[u8]) -> Result { } if oid == &p2p_ext_oid { - // The public host key and the signature are ANS.1-encoded + // The public host key and the signature are ASN.1-encoded // into the SignedKey data structure, which is carried // in the libp2p Public Key Extension. // SignedKey ::= SEQUENCE { @@ -254,7 +254,7 @@ fn make_libp2p_extension( .map_err(|_| rcgen::Error::RingUnspecified)? }; - // The public host key and the signature are ANS.1-encoded + // The public host key and the signature are ASN.1-encoded // into the SignedKey data structure, which is carried // in the libp2p Public Key Extension. // SignedKey ::= SEQUENCE { diff --git a/transports/uds/CHANGELOG.md b/transports/uds/CHANGELOG.md index 44328e019a6..911dab70eca 100644 --- a/transports/uds/CHANGELOG.md +++ b/transports/uds/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.43.1 + +- Rename Config to match naming convention in [discussion 2174](https://github.com/libp2p/rust-libp2p/discussions/2174). + See [PR 6190](https://github.com/libp2p/rust-libp2p/pull/6190). + +## 0.43.0 + +- Remove `async-std` support. + See [PR 6060](https://github.com/libp2p/rust-libp2p/pull/6060) + ## 0.42.0 diff --git a/transports/uds/Cargo.toml b/transports/uds/Cargo.toml index c9e94afa356..82356512f8f 100644 --- a/transports/uds/Cargo.toml +++ b/transports/uds/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-uds" edition.workspace = true rust-version = { workspace = true } description = "Unix domain sockets transport for libp2p" -version = "0.42.0" +version = "0.43.1" authors = ["Parity Technologies "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" @@ -11,7 +11,6 @@ keywords = ["peer-to-peer", "libp2p", "networking"] categories = ["network-programming", "asynchronous"] [dependencies] -async-std = { version = "1.6.2", optional = true } libp2p-core = { workspace = true } futures = { workspace = true } tokio = { workspace = true, default-features = false, features = ["net"], optional = true } diff --git a/transports/uds/src/lib.rs b/transports/uds/src/lib.rs index 2106ddbd6c8..082bc831bc9 100644 --- a/transports/uds/src/lib.rs +++ b/transports/uds/src/lib.rs @@ -26,16 +26,12 @@ //! //! # Usage //! -//! The `UdsConfig` transport supports multiaddresses of the form `/unix//tmp/foo`. +//! The `Config` transport supports multiaddresses of the form `/unix//tmp/foo`. //! -//! The `UdsConfig` structs implements the `Transport` trait of the `core` library. See the +//! The `Config` structs implements the `Transport` trait of the `core` library. See the //! documentation of `core` and of libp2p in general to learn how to use the `Transport` trait. -#![cfg(all( - unix, - not(target_os = "emscripten"), - any(feature = "tokio", feature = "async-std") -))] +#![cfg(all(unix, not(target_os = "emscripten"), feature = "tokio"))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] use std::{ @@ -66,28 +62,28 @@ pub type Listener = BoxStream< >; macro_rules! codegen { - ($feature_name:expr, $uds_config:ident, $build_listener:expr, $unix_stream:ty, $($mut_or_not:tt)*) => { + ($feature_name:expr, $config:ident, $build_listener:expr, $unix_stream:ty, $($mut_or_not:tt)*) => { /// Represents the configuration for a Unix domain sockets transport capability for libp2p. - pub struct $uds_config { + pub struct $config { listeners: VecDeque<(ListenerId, Listener)>, } - impl $uds_config { + impl $config { /// Creates a new configuration object for Unix domain sockets. - pub fn new() -> $uds_config { - $uds_config { + pub fn new() -> $config { + $config { listeners: VecDeque::new(), } } } - impl Default for $uds_config { + impl Default for $config { fn default() -> Self { Self::new() } } - impl Transport for $uds_config { + impl Transport for $config { type Output = $unix_stream; type Error = io::Error; type ListenerUpgrade = Ready>; @@ -205,21 +201,19 @@ macro_rules! codegen { }; } -#[cfg(feature = "async-std")] -codegen!( - "async-std", - UdsConfig, - |addr| async move { async_std::os::unix::net::UnixListener::bind(&addr).await }, - async_std::os::unix::net::UnixStream, -); #[cfg(feature = "tokio")] codegen!( "tokio", - TokioUdsConfig, + Config, |addr| async move { tokio::net::UnixListener::bind(&addr) }, tokio::net::UnixStream, ); +// Deprecated type alias for backward compatibility +#[cfg(feature = "tokio")] +#[deprecated(since = "0.43.1", note = "Use `libp2p::uds::Config` instead")] +pub type TokioUdsConfig = Config; + /// Turns a `Multiaddr` containing a single `Unix` component into a path. /// /// Also returns an error if the path is not absolute, as we don't want to dial/listen on relative @@ -254,7 +248,8 @@ mod tests { }; use tokio::io::{AsyncReadExt, AsyncWriteExt}; - use super::{multiaddr_to_path, TokioUdsConfig}; + use super::multiaddr_to_path; + use crate::Config; #[test] fn multiaddr_to_path_conversion() { @@ -283,7 +278,7 @@ mod tests { let (tx, rx) = oneshot::channel(); let listener = async move { - let mut transport = TokioUdsConfig::new().boxed(); + let mut transport = Config::new().boxed(); transport.listen_on(ListenerId::next(), addr).unwrap(); let listen_addr = transport @@ -307,7 +302,7 @@ mod tests { }; let dialer = async move { - let mut uds = TokioUdsConfig::new(); + let mut uds = Config::new(); let addr = rx.await.unwrap(); let mut socket = uds .dial( @@ -329,7 +324,7 @@ mod tests { #[test] #[ignore] // TODO: for the moment unix addresses fail to parse fn larger_addr_denied() { - let mut uds = TokioUdsConfig::new(); + let mut uds = Config::new(); let addr = "/unix//foo/bar".parse::().unwrap(); assert!(uds.listen_on(ListenerId::next(), addr).is_err()); diff --git a/transports/webrtc/CHANGELOG.md b/transports/webrtc/CHANGELOG.md index 083f4f2893a..d4b7ab41536 100644 --- a/transports/webrtc/CHANGELOG.md +++ b/transports/webrtc/CHANGELOG.md @@ -1,4 +1,9 @@ -## 0.10-alpha +## 0.9.0-alpha.2 + +- reduce allocations by replacing `get_or_insert` with `get_or_insert_with` + See [PR 6136](https://github.com/libp2p/rust-libp2p/pull/6136) + +## 0.9.0-alpha.1 - Bump `webrtc` dependency to `0.12.0`. See [PR 5448](https://github.com/libp2p/rust-libp2p/pull/5448). diff --git a/transports/webrtc/Cargo.toml b/transports/webrtc/Cargo.toml index e150cd334f3..0eb9d948e5d 100644 --- a/transports/webrtc/Cargo.toml +++ b/transports/webrtc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libp2p-webrtc" -version = "0.9.0-alpha" +version = "0.9.0-alpha.2" authors = ["Parity Technologies "] description = "WebRTC transport for libp2p" repository = "https://github.com/libp2p/rust-libp2p" diff --git a/transports/webrtc/src/tokio/connection.rs b/transports/webrtc/src/tokio/connection.rs index 19232707e7f..ee1275ee347 100644 --- a/transports/webrtc/src/tokio/connection.rs +++ b/transports/webrtc/src/tokio/connection.rs @@ -203,27 +203,29 @@ impl StreamMuxer for Connection { cx: &mut Context<'_>, ) -> Poll> { let peer_conn = self.peer_conn.clone(); - let fut = self.outbound_fut.get_or_insert(Box::pin(async move { - let peer_conn = peer_conn.lock().await; + let fut = self.outbound_fut.get_or_insert_with(|| { + Box::pin(async move { + let peer_conn = peer_conn.lock().await; - let data_channel = peer_conn.create_data_channel("", None).await?; + let data_channel = peer_conn.create_data_channel("", None).await?; - // No need to hold the lock during the DTLS handshake. - drop(peer_conn); + // No need to hold the lock during the DTLS handshake. + drop(peer_conn); - tracing::trace!(channel=%data_channel.id(), "Opening data channel"); + tracing::trace!(channel=%data_channel.id(), "Opening data channel"); - let (tx, rx) = oneshot::channel::>(); + let (tx, rx) = oneshot::channel::>(); - // Wait until the data channel is opened and detach it. - register_data_channel_open_handler(data_channel, tx).await; + // Wait until the data channel is opened and detach it. + register_data_channel_open_handler(data_channel, tx).await; - // Wait until data channel is opened and ready to use - match rx.await { - Ok(detached) => Ok(detached), - Err(e) => Err(Error::Internal(e.to_string())), - } - })); + // Wait until data channel is opened and ready to use + match rx.await { + Ok(detached) => Ok(detached), + Err(e) => Err(Error::Internal(e.to_string())), + } + }) + }); match ready!(fut.as_mut().poll(cx)) { Ok(detached) => { @@ -250,12 +252,14 @@ impl StreamMuxer for Connection { tracing::debug!("Closing connection"); let peer_conn = self.peer_conn.clone(); - let fut = self.close_fut.get_or_insert(Box::pin(async move { - let peer_conn = peer_conn.lock().await; - peer_conn.close().await?; + let fut = self.close_fut.get_or_insert_with(|| { + Box::pin(async move { + let peer_conn = peer_conn.lock().await; + peer_conn.close().await?; - Ok(()) - })); + Ok(()) + }) + }); match ready!(fut.as_mut().poll(cx)) { Ok(()) => { diff --git a/transports/webrtc/src/tokio/upgrade.rs b/transports/webrtc/src/tokio/upgrade.rs index b83de3f1527..19be103fab2 100644 --- a/transports/webrtc/src/tokio/upgrade.rs +++ b/transports/webrtc/src/tokio/upgrade.rs @@ -181,7 +181,7 @@ fn setting_engine( // Select only the first address of the local candidates. // See https://github.com/libp2p/rust-libp2p/pull/5448#discussion_r2017418520. - // TODO: remove when https://github.com/webrtc-rs/webrtc/issues/662 get's addressed. + // TODO: remove when https://github.com/webrtc-rs/webrtc/issues/662 gets addressed. se.set_ip_filter(Box::new({ let once = AtomicBool::new(true); move |_ip| { diff --git a/transports/websocket-websys/src/lib.rs b/transports/websocket-websys/src/lib.rs index 72f4068610d..aad50877506 100644 --- a/transports/websocket-websys/src/lib.rs +++ b/transports/websocket-websys/src/lib.rs @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -//! Libp2p websocket transports built on [web-sys](https://rustwasm.github.io/wasm-bindgen/web-sys/index.html). +//! Libp2p websocket transports built on [web-sys](https://wasm-bindgen.github.io/wasm-bindgen/contributing/web-sys/index.html). #![allow(unexpected_cfgs)] diff --git a/transports/websocket/CHANGELOG.md b/transports/websocket/CHANGELOG.md index 98cf7d76a9c..4c40e242ca1 100644 --- a/transports/websocket/CHANGELOG.md +++ b/transports/websocket/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.45.2 + +- feat(websocket): support `/tls/sni//ws` multiaddrs in the WebSocket transport + See [PR 6041](https://github.com/your-username/rust-libp2p/pull/6041) + ## 0.45.1 - Rename types to match naming convention in [discussion 2174](https://github.com/libp2p/rust-libp2p/discussions/2174). See [PR 5873](https://github.com/libp2p/rust-libp2p/pull/5873). diff --git a/transports/websocket/Cargo.toml b/transports/websocket/Cargo.toml index 281e5285bde..4659fde58cd 100644 --- a/transports/websocket/Cargo.toml +++ b/transports/websocket/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-websocket" edition.workspace = true rust-version = { workspace = true } description = "WebSocket transport for libp2p" -version = "0.45.1" +version = "0.45.2" authors = ["Parity Technologies "] license = "MIT" repository = "https://github.com/libp2p/rust-libp2p" diff --git a/transports/websocket/src/framed.rs b/transports/websocket/src/framed.rs index 7a1a2bc2714..d146bcf7166 100644 --- a/transports/websocket/src/framed.rs +++ b/transports/websocket/src/framed.rs @@ -508,7 +508,7 @@ fn parse_ws_dial_addr(addr: Multiaddr) -> Result> { let mut protocols = addr.iter(); let mut ip = protocols.next(); let mut tcp = protocols.next(); - let (host_port, server_name) = loop { + let (host_port, mut server_name) = loop { match (ip, tcp) { (Some(Protocol::Ip4(ip)), Some(Protocol::Tcp(port))) => { let server_name = ServerName::IpAddress(IpAddr::V4(ip).into()); @@ -531,6 +531,8 @@ fn parse_ws_dial_addr(addr: Multiaddr) -> Result> { } }; + // Will hold a value if the multiaddr carries `/tls/sni/`. + let mut sni_override: Option> = None; // Now consume the `Ws` / `Wss` protocol from the end of the address, // preserving the trailing `P2p` protocol that identifies the remote, // if any. @@ -540,6 +542,13 @@ fn parse_ws_dial_addr(addr: Multiaddr) -> Result> { match protocols.pop() { p @ Some(Protocol::P2p(_)) => p2p = p, Some(Protocol::Ws(path)) => match protocols.pop() { + Some(Protocol::Sni(domain)) => match protocols.pop() { + Some(Protocol::Tls) => { + sni_override = Some(tls::dns_name_ref(&domain)?); + break (true, path.into_owned()); + } + _ => return Err(Error::InvalidMultiaddr(addr)), + }, Some(Protocol::Tls) => break (true, path.into_owned()), Some(p) => { protocols.push(p); @@ -559,6 +568,9 @@ fn parse_ws_dial_addr(addr: Multiaddr) -> Result> { None => protocols, }; + if let Some(name) = sni_override { + server_name = name; + } Ok(WsAddress { host_port, server_name, @@ -1013,5 +1025,49 @@ mod tests { // Check non-ws address let addr = "/ip4/127.0.0.1/tcp/2222".parse::().unwrap(); parse_ws_dial_addr::(addr).unwrap_err(); + + // Check `/tls/sni/.../ws` with `/dns4` + let addr = "/dns4/example.com/tcp/2222/tls/sni/example.com/ws" + .parse::() + .unwrap(); + let info = parse_ws_dial_addr::(addr).unwrap(); + assert_eq!(info.host_port, "example.com:2222"); + assert_eq!(info.path, "/"); + assert!(info.use_tls); + assert_eq!(info.server_name, "example.com".try_into().unwrap()); + assert_eq!(info.tcp_addr, "/dns4/example.com/tcp/2222".parse().unwrap()); + + // Check `/tls/sni/.../ws` with `/ip4` + let addr = "/ip4/127.0.0.1/tcp/2222/tls/sni/example.test/ws" + .parse::() + .unwrap(); + let info = parse_ws_dial_addr::(addr).unwrap(); + assert_eq!(info.host_port, "127.0.0.1:2222"); + assert_eq!(info.path, "/"); + assert!(info.use_tls); + assert_eq!(info.server_name, "example.test".try_into().unwrap()); + assert_eq!(info.tcp_addr, "/ip4/127.0.0.1/tcp/2222".parse().unwrap()); + + // Check `/tls/sni/.../ws` with trailing `/p2p` + let addr = format!("/dns4/example.com/tcp/2222/tls/sni/example.com/ws/p2p/{peer_id}") + .parse() + .unwrap(); + let info = parse_ws_dial_addr::(addr).unwrap(); + assert_eq!(info.host_port, "example.com:2222"); + assert_eq!(info.path, "/"); + assert!(info.use_tls); + assert_eq!(info.server_name, "example.com".try_into().unwrap()); + assert_eq!( + info.tcp_addr, + format!("/dns4/example.com/tcp/2222/p2p/{peer_id}") + .parse() + .unwrap() + ); + + // Negative: `/tls/sni/...` *without* `/ws` → error + let bad = "/dns4/example.com/tcp/2222/tls/sni/example.com" + .parse::() + .unwrap(); + parse_ws_dial_addr::(bad).unwrap_err(); } } diff --git a/transports/websocket/src/quicksink.rs b/transports/websocket/src/quicksink.rs index 27630949565..bc3767d9682 100644 --- a/transports/websocket/src/quicksink.rs +++ b/transports/websocket/src/quicksink.rs @@ -17,8 +17,8 @@ // # Examples // // ```no_run -// use async_std::io; // use futures::prelude::*; +// use tokio::io; // // use crate::quicksink::Action; // diff --git a/transports/webtransport-websys/CHANGELOG.md b/transports/webtransport-websys/CHANGELOG.md index e455e5ec6c3..e94220bc23e 100644 --- a/transports/webtransport-websys/CHANGELOG.md +++ b/transports/webtransport-websys/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.5.2 + +- Remove `poll_flush called after poll_close` assertion. + See [PR 6193](https://github.com/libp2p/rust-libp2p/pull/6193). + ## 0.5.1 - Remove `once_cell` dependency. diff --git a/transports/webtransport-websys/Cargo.toml b/transports/webtransport-websys/Cargo.toml index e577ea74896..bcdf317a3ae 100644 --- a/transports/webtransport-websys/Cargo.toml +++ b/transports/webtransport-websys/Cargo.toml @@ -3,7 +3,7 @@ name = "libp2p-webtransport-websys" edition.workspace = true rust-version = { workspace = true } description = "WebTransport for libp2p under WASM environment" -version = "0.5.1" +version = "0.5.2" authors = [ "Yiannis Marangos ", "oblique ", diff --git a/transports/webtransport-websys/src/lib.rs b/transports/webtransport-websys/src/lib.rs index 126adc054a9..8381467737d 100644 --- a/transports/webtransport-websys/src/lib.rs +++ b/transports/webtransport-websys/src/lib.rs @@ -1,4 +1,4 @@ -//! Libp2p WebTransport built on [web-sys](https://rustwasm.github.io/wasm-bindgen/web-sys/index.html) +//! Libp2p WebTransport built on [web-sys](https://wasm-bindgen.github.io/wasm-bindgen/contributing/web-sys/index.html) #![allow(unexpected_cfgs)] diff --git a/transports/webtransport-websys/src/stream.rs b/transports/webtransport-websys/src/stream.rs index b9d1669b6dc..18de06e3d8f 100644 --- a/transports/webtransport-websys/src/stream.rs +++ b/transports/webtransport-websys/src/stream.rs @@ -153,10 +153,6 @@ impl StreamInner { // messages were flushed. self.poll_writer_ready(cx) } else { - debug_assert!( - false, - "libp2p_webtransport_websys::Stream: poll_flush called after poll_close" - ); Poll::Ready(Ok(())) } } diff --git a/wasm-tests/webtransport-tests/echo-server/go.mod b/wasm-tests/webtransport-tests/echo-server/go.mod index 4ffb98f386c..a3576a42489 100644 --- a/wasm-tests/webtransport-tests/echo-server/go.mod +++ b/wasm-tests/webtransport-tests/echo-server/go.mod @@ -1,11 +1,11 @@ module echo-server -go 1.24 +go 1.24.6 require ( - github.com/libp2p/go-libp2p v0.41.0 - github.com/multiformats/go-multiaddr v0.15.0 - github.com/quic-go/quic-go v0.50.1 + github.com/libp2p/go-libp2p v0.44.0 + github.com/multiformats/go-multiaddr v0.16.1 + github.com/quic-go/quic-go v0.55.0 ) require ( @@ -16,46 +16,45 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/gopacket v1.1.19 // indirect - github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect github.com/ipfs/go-cid v0.5.0 // indirect github.com/ipfs/go-log/v2 v2.5.1 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect - github.com/libp2p/go-netroute v0.2.2 // indirect + github.com/libp2p/go-netroute v0.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-multibase v0.2.0 // indirect - github.com/multiformats/go-multicodec v0.9.0 // indirect + github.com/multiformats/go-multicodec v0.10.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-multistream v0.6.0 // indirect - github.com/multiformats/go-varint v0.0.7 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.23.0 // indirect - github.com/prometheus/client_golang v1.21.1 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.63.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect + github.com/quic-go/webtransport-go v0.9.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - go.uber.org/mock v0.5.0 // indirect + go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.31.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - lukechampine.com/blake3 v1.4.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/exp v0.0.0-20251017212417-90e834f514db // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + lukechampine.com/blake3 v1.4.1 // indirect ) diff --git a/wasm-tests/webtransport-tests/echo-server/go.sum b/wasm-tests/webtransport-tests/echo-server/go.sum index 10c8992f9a3..a994475b9f4 100644 --- a/wasm-tests/webtransport-tests/echo-server/go.sum +++ b/wasm-tests/webtransport-tests/echo-server/go.sum @@ -40,10 +40,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= @@ -61,8 +57,6 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= -github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -80,8 +74,12 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -95,10 +93,14 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-libp2p v0.41.0 h1:JRaD39dqf/tBBGapJ0T38N73vOaDCsWgcx3mE6HgXWk= github.com/libp2p/go-libp2p v0.41.0/go.mod h1:Be8QYqC4JW6Xq8buukNeoZJjyT1XUDcGoIooCHm1ye4= +github.com/libp2p/go-libp2p v0.44.0 h1:5Gtt8OrF8yiXmH+Mx4+/iBeFRMK1TY3a8OrEBDEqAvs= +github.com/libp2p/go-libp2p v0.44.0/go.mod h1:NovCojezAt4dnDd4fH048K7PKEqH0UFYYqJRjIIu8zc= github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8= github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE= +github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc= +github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po= github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= @@ -120,24 +122,28 @@ github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9 github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/multiformats/go-multiaddr v0.15.0 h1:zB/HeaI/apcZiTDwhY5YqMvNVl/oQYvs3XySU+qeAVo= github.com/multiformats/go-multiaddr v0.15.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/onsi/ginkgo/v2 v2.23.0 h1:FA1xjp8ieYDzlgS5ABTpdUDB7wtngggONc8a7ku2NqQ= -github.com/onsi/ginkgo/v2 v2.23.0/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= -github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= -github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -145,21 +151,33 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= -github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= +github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg= +github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= +github.com/quic-go/webtransport-go v0.9.0 h1:jgys+7/wm6JarGDrW+lD/r9BGqBAmqY/ssklE09bA70= +github.com/quic-go/webtransport-go v0.9.0/go.mod h1:4FUYIiUc75XSsF6HShcLeXXYZJ9AGwo/xh3L8M/P1ao= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -196,6 +214,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= @@ -207,12 +226,15 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -223,9 +245,13 @@ golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw= +golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -235,6 +261,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -250,6 +278,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -263,6 +293,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -277,16 +309,20 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -298,6 +334,8 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -319,6 +357,8 @@ google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -337,5 +377,7 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w= lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=